mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Using a different OpenAPI dependency for hono for now, adding docs for IAM.
This commit is contained in:
parent
47ae91e015
commit
e48c9b3e09
12 changed files with 317 additions and 203 deletions
|
|
@ -1,26 +1,40 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import * as HttpStatusPhrases from 'stoker/http-status-phrases';
|
||||
import { createMessageObjectSchema } from 'stoker/openapi/schemas';
|
||||
|
||||
export function TooManyRequests(message = 'Too many requests') {
|
||||
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message })
|
||||
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });
|
||||
}
|
||||
|
||||
export const tooManyRequestsSchema = createMessageObjectSchema(HttpStatusPhrases.TOO_MANY_REQUESTS);
|
||||
|
||||
export function Forbidden(message = 'Forbidden') {
|
||||
return new HTTPException(StatusCodes.FORBIDDEN, { message })
|
||||
return new HTTPException(StatusCodes.FORBIDDEN, { message });
|
||||
}
|
||||
|
||||
export const forbiddenSchema = createMessageObjectSchema(HttpStatusPhrases.FORBIDDEN);
|
||||
|
||||
export function Unauthorized(message = 'Unauthorized') {
|
||||
return new HTTPException(StatusCodes.UNAUTHORIZED, { message })
|
||||
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
|
||||
}
|
||||
|
||||
export const unauthorizedSchema = createMessageObjectSchema(HttpStatusPhrases.UNAUTHORIZED);
|
||||
|
||||
export function NotFound(message = 'Not Found') {
|
||||
return new HTTPException(StatusCodes.NOT_FOUND, { message })
|
||||
return new HTTPException(StatusCodes.NOT_FOUND, { message });
|
||||
}
|
||||
|
||||
export const notFoundSchema = createMessageObjectSchema(HttpStatusPhrases.NOT_FOUND);
|
||||
|
||||
export function BadRequest(message = 'Bad Request') {
|
||||
return new HTTPException(StatusCodes.BAD_REQUEST, { message })
|
||||
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
|
||||
}
|
||||
|
||||
export const badRequestSchema = createMessageObjectSchema(HttpStatusPhrases.BAD_REQUEST);
|
||||
|
||||
export function InternalError(message = 'Internal Error') {
|
||||
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message })
|
||||
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
|
||||
}
|
||||
|
||||
export const internalErrorSchema = createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Hono } from 'hono'
|
||||
import type { BlankSchema } from 'hono/types'
|
||||
import type { HonoTypes } from './hono'
|
||||
import { Hono } from 'hono';
|
||||
import type { BlankSchema } from 'hono/types';
|
||||
import type { AppBindings } from './hono';
|
||||
|
||||
export abstract class Controller {
|
||||
protected readonly controller: Hono<HonoTypes, BlankSchema, '/'>
|
||||
protected readonly controller: Hono<AppBindings, BlankSchema, '/'>;
|
||||
constructor() {
|
||||
this.controller = new Hono()
|
||||
this.controller = new Hono();
|
||||
}
|
||||
abstract routes(): Hono<HonoTypes, BlankSchema, '/'>
|
||||
abstract routes(): Hono<AppBindings, BlankSchema, '/'>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import type { Hono } from 'hono';
|
||||
import type { PinoLogger } from 'hono-pino';
|
||||
import type { Promisify, RateLimitInfo } from 'hono-rate-limiter';
|
||||
import type { Session, User } from 'lucia';
|
||||
|
||||
// export type AppOpenAPI = OpenAPIHono<AppBindings>;
|
||||
export type AppOpenAPI = Hono;
|
||||
|
||||
export type AppBindings = {
|
||||
Variables: {
|
||||
logger: PinoLogger;
|
||||
|
|
|
|||
|
|
@ -1,42 +1,59 @@
|
|||
// // import type { AppOpenAPI } from '$lib/server/api/common/types/hono';
|
||||
// import { apiReference } from '@scalar/hono-api-reference';
|
||||
// import { Hono } from 'hono';
|
||||
//
|
||||
// import type { AppBindings } from '$lib/server/api/common/types/hono';
|
||||
// import type { AppOpenAPI } from '$lib/server/api/common/types/hono';
|
||||
import { apiReference } from '@scalar/hono-api-reference';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import type { AppOpenAPI } from '$lib/server/api/common/types/hono';
|
||||
// import { createOpenApiDocument } from 'hono-zod-openapi';
|
||||
// import packageJSON from '../../../../package.json';
|
||||
//
|
||||
// // export default function configureOpenAPI(app: AppOpenAPI) {
|
||||
// // app.doc('/doc', {
|
||||
// // openapi: '3.0.0',
|
||||
// // info: {
|
||||
// // title: 'Bored Game API',
|
||||
// // description: 'Bored Game API',
|
||||
// // version: packageJSON.version,
|
||||
// // },
|
||||
// // });
|
||||
// //
|
||||
// // app.get(
|
||||
// // '/reference',
|
||||
// // apiReference({
|
||||
// // theme: 'kepler',
|
||||
// // layout: 'classic',
|
||||
// // defaultHttpClient: {
|
||||
// // targetKey: 'javascript',
|
||||
// // clientKey: 'fetch',
|
||||
// // },
|
||||
// // spec: {
|
||||
// // url: '/api/doc',
|
||||
// // },
|
||||
// // }),
|
||||
// // );
|
||||
// // }
|
||||
//
|
||||
// export default function configureOpenAPI(app: Hono<AppBindings>) {
|
||||
// createOpenApiDocument(app, {
|
||||
import { createOpenApiDocument } from 'hono-zod-openapi';
|
||||
import packageJSON from '../../../../package.json';
|
||||
|
||||
// export default function configureOpenAPI(app: AppOpenAPI) {
|
||||
// app.doc('/doc', {
|
||||
// openapi: '3.0.0',
|
||||
// info: {
|
||||
// title: 'Example API',
|
||||
// version: '1.0.0',
|
||||
// title: 'Bored Game API',
|
||||
// description: 'Bored Game API',
|
||||
// version: packageJSON.version,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// app.get(
|
||||
// '/reference',
|
||||
// apiReference({
|
||||
// theme: 'kepler',
|
||||
// layout: 'classic',
|
||||
// defaultHttpClient: {
|
||||
// targetKey: 'javascript',
|
||||
// clientKey: 'fetch',
|
||||
// },
|
||||
// spec: {
|
||||
// url: '/api/doc',
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
export default function configureOpenAPI(app: AppOpenAPI) {
|
||||
createOpenApiDocument(app, {
|
||||
info: {
|
||||
title: 'Bored Game API',
|
||||
description: 'Bored Game API',
|
||||
version: packageJSON.version,
|
||||
},
|
||||
});
|
||||
|
||||
app.get(
|
||||
'/reference',
|
||||
apiReference({
|
||||
theme: 'kepler',
|
||||
layout: 'classic',
|
||||
defaultHttpClient: {
|
||||
targetKey: 'javascript',
|
||||
clientKey: 'fetch',
|
||||
},
|
||||
spec: {
|
||||
url: '/api/doc',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { Controller } from '$lib/server/api/common/types/controller';
|
||||
import { iam, updateProfile } from '$lib/server/api/controllers/iam.routes';
|
||||
import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto';
|
||||
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
|
||||
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
|
||||
|
|
@ -9,6 +10,7 @@ import { IamService } from '$lib/server/api/services/iam.service';
|
|||
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service';
|
||||
import { LuciaService } from '$lib/server/api/services/lucia.service';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { openApi } from 'hono-zod-openapi';
|
||||
import { setCookie } from 'hono/cookie';
|
||||
import { inject, injectable } from 'tsyringe';
|
||||
import { requireAuth } from '../middleware/require-auth.middleware';
|
||||
|
|
@ -24,22 +26,27 @@ export class IamController extends Controller {
|
|||
}
|
||||
|
||||
routes() {
|
||||
const tags = ['IAM'];
|
||||
|
||||
return this.controller
|
||||
.get('/', requireAuth, async (c) => {
|
||||
.get('/', requireAuth, openApi(iam), async (c) => {
|
||||
const user = c.var.user;
|
||||
return c.json({ user });
|
||||
})
|
||||
.put('/update/profile', requireAuth, zValidator('json', updateProfileDto), limiter({ limit: 30, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user;
|
||||
const { firstName, lastName, username } = c.req.valid('json');
|
||||
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username });
|
||||
if (!updatedUser) {
|
||||
return c.json('Username already in use', StatusCodes.BAD_REQUEST);
|
||||
}
|
||||
return c.json({ user: updatedUser }, StatusCodes.OK);
|
||||
})
|
||||
.put(
|
||||
'/update/profile',
|
||||
requireAuth,
|
||||
openApi(updateProfile),
|
||||
zValidator('json', updateProfileDto),
|
||||
limiter({ limit: 30, minutes: 60 }),
|
||||
async (c) => {
|
||||
const user = c.var.user;
|
||||
const { firstName, lastName, username } = c.req.valid('json');
|
||||
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username });
|
||||
if (!updatedUser) {
|
||||
return c.json('Username already in use', StatusCodes.BAD_REQUEST);
|
||||
}
|
||||
return c.json({ user: updatedUser }, StatusCodes.OK);
|
||||
},
|
||||
)
|
||||
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user;
|
||||
const { password } = c.req.valid('json');
|
||||
|
|
|
|||
48
src/lib/server/api/controllers/iam.routes.ts
Normal file
48
src/lib/server/api/controllers/iam.routes.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { unauthorizedSchema } from '$lib/server/api/common/exceptions';
|
||||
import { selectUserSchema } from '$lib/server/api/databases/tables/users.table';
|
||||
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
|
||||
import { defineOpenApiOperation } from 'hono-zod-openapi';
|
||||
import { createErrorSchema } from 'stoker/openapi/schemas';
|
||||
|
||||
const tags = ['IAM'];
|
||||
|
||||
export const iam = defineOpenApiOperation({
|
||||
tags,
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'User profile',
|
||||
schema: selectUserSchema,
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updateProfile = defineOpenApiOperation({
|
||||
tags,
|
||||
request: {
|
||||
json: updateProfileDto,
|
||||
},
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'Updated User',
|
||||
schema: selectUserSchema,
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNPROCESSABLE_ENTITY]: {
|
||||
description: 'The validation error(s)',
|
||||
schema: createErrorSchema(updateProfileDto),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { createId as cuid2 } from '@paralleldrive/cuid2'
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm'
|
||||
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
|
||||
import { timestamps } from '../../common/utils/table'
|
||||
import { user_roles } from './userRoles.table'
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createSelectSchema } from 'drizzle-zod';
|
||||
import { timestamps } from '../../common/utils/table';
|
||||
import { user_roles } from './userRoles.table';
|
||||
|
||||
export const usersTable = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
|
@ -20,10 +21,12 @@ export const usersTable = pgTable('users', {
|
|||
mfa_enabled: boolean('mfa_enabled').notNull().default(false),
|
||||
theme: text('theme').default('system'),
|
||||
...timestamps,
|
||||
})
|
||||
});
|
||||
|
||||
export const userRelations = relations(usersTable, ({ many }) => ({
|
||||
user_roles: many(user_roles),
|
||||
}))
|
||||
}));
|
||||
|
||||
export type Users = InferSelectModel<typeof usersTable>
|
||||
export const selectUserSchema = createSelectSchema(usersTable);
|
||||
|
||||
export type Users = InferSelectModel<typeof usersTable>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'reflect-metadata';
|
||||
import createApp from '$lib/server/api/common/create-app';
|
||||
import configureOpenAPI from '$lib/server/api/configure-open-api';
|
||||
import { CollectionController } from '$lib/server/api/controllers/collection.controller';
|
||||
import { MfaController } from '$lib/server/api/controllers/mfa.controller';
|
||||
import { OAuthController } from '$lib/server/api/controllers/oauth.controller';
|
||||
|
|
@ -7,15 +8,17 @@ import { SignupController } from '$lib/server/api/controllers/signup.controller'
|
|||
import { UserController } from '$lib/server/api/controllers/user.controller';
|
||||
import { WishlistController } from '$lib/server/api/controllers/wishlist.controller';
|
||||
import { AuthCleanupJobs } from '$lib/server/api/jobs/auth-cleanup.job';
|
||||
import { extendZodWithOpenApi } from 'hono-zod-openapi';
|
||||
import { hc } from 'hono/client';
|
||||
import { container } from 'tsyringe';
|
||||
import { z } from 'zod';
|
||||
import { config } from './common/config';
|
||||
import { IamController } from './controllers/iam.controller';
|
||||
import { LoginController } from './controllers/login.controller';
|
||||
|
||||
export const app = createApp();
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
// configureOpenAPI(app);
|
||||
export const app = createApp();
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Routes */
|
||||
|
|
@ -31,6 +34,9 @@ const routes = app
|
|||
.route('/mfa', container.resolve(MfaController).routes())
|
||||
.get('/', (c) => c.json({ message: 'Server is healthy' }));
|
||||
|
||||
// @ts-ignore - this is a workaround for https://github.com/paolostyle/hono-zod-openapi/issues/2
|
||||
configureOpenAPI(app);
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Cron Jobs */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { usersTable } from '$lib/server/api/databases/tables/users.table'
|
||||
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
|
||||
import { type InferInsertModel, eq } from 'drizzle-orm'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { takeFirstOrThrow } from '../common/utils/repository'
|
||||
import { usersTable } from '$lib/server/api/databases/tables/users.table';
|
||||
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
|
||||
import { type InferInsertModel, eq } from 'drizzle-orm';
|
||||
import { inject, injectable } from 'tsyringe';
|
||||
import { takeFirstOrThrow } from '../common/utils/repository';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Repository */
|
||||
|
|
@ -20,8 +20,8 @@ storing data. They should not contain any business logic, only database queries.
|
|||
In our case the method 'trxHost' is used to set the transaction context.
|
||||
*/
|
||||
|
||||
export type CreateUser = InferInsertModel<typeof usersTable>
|
||||
export type UpdateUser = Partial<CreateUser>
|
||||
export type CreateUser = InferInsertModel<typeof usersTable>;
|
||||
export type UpdateUser = Partial<CreateUser>;
|
||||
|
||||
@injectable()
|
||||
export class UsersRepository {
|
||||
|
|
@ -30,36 +30,36 @@ export class UsersRepository {
|
|||
async findOneById(id: string, db = this.drizzle.db) {
|
||||
return db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, id),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
||||
const user = await this.findOneById(id)
|
||||
if (!user) throw Error('User not found')
|
||||
return user
|
||||
const user = await this.findOneById(id);
|
||||
if (!user) throw Error('User not found');
|
||||
return user;
|
||||
}
|
||||
|
||||
async findOneByUsername(username: string, db = this.drizzle.db) {
|
||||
return db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.username, username),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async findOneByEmail(email: string, db = this.drizzle.db) {
|
||||
return db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.email, email),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateUser, db = this.drizzle.db) {
|
||||
return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow)
|
||||
return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow);
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateUser, db = this.drizzle.db) {
|
||||
return db.update(usersTable).set(data).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow)
|
||||
return db.update(usersTable).set(data).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow);
|
||||
}
|
||||
|
||||
async delete(id: string, db = this.drizzle.db) {
|
||||
return db.delete(usersTable).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow)
|
||||
return db.delete(usersTable).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +1,102 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { usersTable } from '$lib/server/api/databases/tables'
|
||||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
import { type Actions, fail } from '@sveltejs/kit'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
import { zod } from 'sveltekit-superforms/adapters'
|
||||
import { message, setError, superValidate } from 'sveltekit-superforms/server'
|
||||
import { z } from 'zod'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import { updateEmailSchema, updateProfileSchema } from './schemas'
|
||||
import { notSignedInMessage } from '$lib/flashMessages';
|
||||
import { usersTable } from '$lib/server/api/databases/tables';
|
||||
import { db } from '$lib/server/api/packages/drizzle';
|
||||
import { type Actions, fail } from '@sveltejs/kit';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { message, setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { z } from 'zod';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { updateEmailFormSchema, updateProfileFormSchema } from './schemas';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event
|
||||
const { locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const profileForm = await superValidate(zod(updateProfileSchema), {
|
||||
const updateProfileForm = await superValidate(zod(updateProfileFormSchema), {
|
||||
defaults: {
|
||||
firstName: authedUser?.firstName ?? '',
|
||||
lastName: authedUser?.lastName ?? '',
|
||||
username: authedUser?.username ?? '',
|
||||
},
|
||||
})
|
||||
const emailForm = await superValidate(zod(updateEmailSchema), {
|
||||
});
|
||||
const updateEmailForm = await superValidate(zod(updateEmailFormSchema), {
|
||||
defaults: {
|
||||
email: authedUser?.email ?? '',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// const twoFactorDetails = await db.query.twoFactor.findFirst({
|
||||
// where: eq(twoFactor.userId, authedUser!.id!),
|
||||
// });
|
||||
|
||||
return {
|
||||
profileForm,
|
||||
emailForm,
|
||||
updateProfileForm,
|
||||
updateEmailForm,
|
||||
hasSetupTwoFactor: false, //!!twoFactorDetails?.enabled,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const changeEmailIfNotEmpty = z.object({
|
||||
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).email({ message: 'Please enter a valid email' }),
|
||||
})
|
||||
});
|
||||
|
||||
export const actions: Actions = {
|
||||
profileUpdate: async (event) => {
|
||||
const { locals } = event
|
||||
const { locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
|
||||
if (!authedUser) {
|
||||
redirect(302, '/login', notSignedInMessage, event)
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(updateProfileSchema))
|
||||
const form = await superValidate(event, zod(updateProfileFormSchema));
|
||||
|
||||
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse)
|
||||
console.log('data from profile update', error)
|
||||
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse);
|
||||
console.log('data from profile update', error);
|
||||
if (error) {
|
||||
return setError(form, 'username', error)
|
||||
return setError(form, 'username', error);
|
||||
}
|
||||
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
console.log('profile updated successfully')
|
||||
return message(form, { type: 'success', message: 'Profile updated successfully!' })
|
||||
console.log('profile updated successfully');
|
||||
return message(form, { type: 'success', message: 'Profile updated successfully!' });
|
||||
},
|
||||
changeEmail: async (event) => {
|
||||
const form = await superValidate(event, zod(updateEmailSchema))
|
||||
const form = await superValidate(event, zod(updateEmailFormSchema));
|
||||
|
||||
const newEmail = form.data?.email
|
||||
const newEmail = form.data?.email;
|
||||
if (!form.valid || !newEmail || (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)) {
|
||||
return fail(400, {
|
||||
form,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event)
|
||||
redirect(302, '/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
const user = event.locals.user
|
||||
const user = event.locals.user;
|
||||
const existingUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.email, newEmail),
|
||||
})
|
||||
});
|
||||
|
||||
if (existingUser && existingUser.id !== user.id) {
|
||||
return setError(form, 'email', 'That email is already taken')
|
||||
return setError(form, 'email', 'That email is already taken');
|
||||
}
|
||||
|
||||
await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id))
|
||||
await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id));
|
||||
|
||||
// if (user.email !== form.data.email) {
|
||||
// Send email to confirm new email?
|
||||
|
|
@ -114,6 +114,6 @@ export const actions: Actions = {
|
|||
// });
|
||||
// }
|
||||
|
||||
return message(form, { type: 'success', message: 'Email updated successfully!' })
|
||||
return message(form, { type: 'success', message: 'Email updated successfully!' });
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,26 +1,47 @@
|
|||
<script context="module" lang="ts">
|
||||
import type { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
|
||||
import type { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
|
||||
import type { Infer, SuperValidated } from 'sveltekit-superforms';
|
||||
|
||||
interface UpdateProfileProps {
|
||||
updateEmailForm: SuperValidated<Infer<typeof updateEmailDto>>;
|
||||
updateProfileForm: SuperValidated<Infer<typeof updateProfileDto>>;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Alert from '$components/ui/alert'
|
||||
import * as Form from '$components/ui/form'
|
||||
import { Button } from '$components/ui/button'
|
||||
import { Input } from '$components/ui/input'
|
||||
// import * as Form from '$lib/components/ui/form';
|
||||
import { Label } from '$components/ui/label'
|
||||
import { AlertTriangle, KeyRound } from 'lucide-svelte'
|
||||
import * as flashModule from 'sveltekit-flash-message/client'
|
||||
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||
import { superForm } from 'sveltekit-superforms/client'
|
||||
import { updateEmailSchema, updateProfileSchema } from './schemas'
|
||||
import { updateEmailFormSchema, updateProfileFormSchema } from './schemas'
|
||||
|
||||
const { data } = $props()
|
||||
const { updateEmailForm, updateProfileForm }: UpdateProfileProps = data;
|
||||
|
||||
const {
|
||||
form: profileForm,
|
||||
errors: profileErrors,
|
||||
enhance: profileEnhance,
|
||||
} = superForm(data.profileForm, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(updateProfileSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
const sf_updateProfileForm = superForm(updateProfileForm, {
|
||||
validators: zodClient(updateProfileFormSchema),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => {
|
||||
if (!form.valid) return;
|
||||
},
|
||||
syncFlashMessage: true,
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
},
|
||||
})
|
||||
|
||||
const sf_updateEmailForm = superForm(updateEmailForm, {
|
||||
validators: zodClient(updateEmailFormSchema),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => {
|
||||
if (!form.valid) return;
|
||||
},
|
||||
syncFlashMessage: true,
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
|
|
@ -28,66 +49,60 @@ const {
|
|||
})
|
||||
|
||||
const {
|
||||
form: emailForm,
|
||||
form: updateProfileFormData,
|
||||
submit: submitProfileForm,
|
||||
enhance: updateProfileFormEnhance,
|
||||
} = sf_updateProfileForm;
|
||||
|
||||
const {
|
||||
form: updateEmailFormData,
|
||||
submit: submitEmailForm,
|
||||
enhance: updateEmailFormEnhance,
|
||||
errors: emailErrors,
|
||||
enhance: emailEnhance,
|
||||
} = superForm(data.emailForm, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(updateEmailSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
syncFlashMessage: true,
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
},
|
||||
})
|
||||
} = sf_updateEmailForm;
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/profileUpdate" use:profileEnhance>
|
||||
<form method="POST" action="?/profileUpdate" use:updateProfileFormEnhance>
|
||||
<h3>Your Profile</h3>
|
||||
<hr class="!border-t-2 mt-2 mb-6" />
|
||||
<div class="mt-6">
|
||||
<Label for="username">Username</Label>
|
||||
<Input type="text" id="username" name="username" placeholder="Username" autocomplete="username" data-invalid={$profileErrors.username} bind:value={$profileForm.username} />
|
||||
{#if $profileErrors.username}
|
||||
<small>{$profileErrors.username}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<Label for="firstName">First Name</Label>
|
||||
<Input type="text" id="firstName" name="firstName" placeholder="First Name" autocomplete="given-name" data-invalid={$profileErrors.firstName} bind:value={$profileForm.firstName} />
|
||||
{#if $profileErrors.firstName}
|
||||
<small>{$profileErrors.firstName}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<Label for="lastName">Last Name</Label>
|
||||
<Input type="text" id="lastName" name="lastName" placeholder="Last Name" autocomplete="family-name" data-invalid={$profileErrors.lastName} bind:value={$profileForm.lastName} />
|
||||
{#if $profileErrors.lastName}
|
||||
<small>{$profileErrors.lastName}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<Button type="submit" class="w-full mt-3">Update Profile</Button>
|
||||
<Form.Field form={sf_updateProfileForm} name="username">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="username">Username</Form.Label>
|
||||
<Input {...attrs} bind:value={$updateProfileFormData.username} />
|
||||
<Form.Description />
|
||||
<Form.FieldErrors />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field form={sf_updateProfileForm} name="firstName">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="firstName">First Name</Form.Label>
|
||||
<Input {...attrs} bind:value={$updateProfileFormData.firstName} />
|
||||
<Form.Description />
|
||||
<Form.FieldErrors />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field form={sf_updateProfileForm} name="lastName">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="lastName">Last Name</Form.Label>
|
||||
<Input {...attrs} bind:value={$updateProfileFormData.lastName} />
|
||||
<Form.Description />
|
||||
<Form.FieldErrors />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Button on:click={() => submitProfileForm()} class="w-full">Update Profile</Form.Button>
|
||||
</form>
|
||||
<form method="POST" action="?/changeEmail" use:emailEnhance>
|
||||
<form method="POST" action="?/changeEmail" use:updateEmailFormEnhance>
|
||||
<div class="grid gap-2 mt-6">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
autocomplete="email"
|
||||
data-invalid={$emailErrors.email}
|
||||
bind:value={$emailForm.email}
|
||||
/>
|
||||
{#if $emailErrors.email}
|
||||
<small>{$emailErrors.email}</small>
|
||||
{/if}
|
||||
<Button type="submit" class="w-full">Update Email</Button>
|
||||
{#if !$emailForm.email}
|
||||
<Form.Field form={sf_updateEmailForm} name="email">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="email">Email address</Form.Label>
|
||||
<Input {...attrs} bind:value={$updateEmailFormData.email} />
|
||||
<Form.Description />
|
||||
<Form.FieldErrors />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Button on:click={() => submitEmailForm()} class="w-full">Update Email</Form.Button>
|
||||
{#if !$updateEmailFormData.email}
|
||||
<Alert.Root variant="destructive">
|
||||
<AlertTriangle class="h-4 w-4" />
|
||||
<Alert.Title>Heads up!</Alert.Title>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
export const updateEmailSchema = z.object({
|
||||
export const updateEmailFormSchema = z.object({
|
||||
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).email({ message: 'Please enter a valid email' }),
|
||||
})
|
||||
});
|
||||
|
||||
export type UpdateEmailSchema = z.infer<typeof updateEmailSchema>
|
||||
export type UpdateEmailSchema = z.infer<typeof updateEmailFormSchema>;
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
export const updateProfileFormSchema = z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
|
|
@ -15,6 +15,6 @@ export const updateProfileSchema = z.object({
|
|||
.optional(),
|
||||
lastName: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
|
||||
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||
})
|
||||
});
|
||||
|
||||
export type UpdateProfileSchema = z.infer<typeof updateProfileSchema>
|
||||
export type UpdateProfileSchema = z.infer<typeof updateProfileFormSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue