mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Changing the security auth openapi to be cookie session and documenting more APIs.
This commit is contained in:
parent
eb1d44037e
commit
cbcbbed912
8 changed files with 163 additions and 38 deletions
|
|
@ -98,7 +98,7 @@
|
|||
"@types/feather-icons": "^4.29.4",
|
||||
"bits-ui": "^0.21.16",
|
||||
"boardgamegeekclient": "^1.9.1",
|
||||
"bullmq": "^5.19.0",
|
||||
"bullmq": "^5.19.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie": "^0.6.0",
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
"hono": "^4.6.4",
|
||||
"hono-pino": "^0.3.0",
|
||||
"hono-rate-limiter": "^0.4.0",
|
||||
"hono-zod-openapi": "^0.2.1",
|
||||
"hono-zod-openapi": "^0.3.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"iconify-icon": "^2.1.0",
|
||||
"ioredis": "^5.4.1",
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ importers:
|
|||
specifier: ^1.9.1
|
||||
version: 1.9.1
|
||||
bullmq:
|
||||
specifier: ^5.19.0
|
||||
version: 5.19.0
|
||||
specifier: ^5.19.1
|
||||
version: 5.19.1
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
|
|
@ -123,8 +123,8 @@ importers:
|
|||
specifier: ^0.4.0
|
||||
version: 0.4.0(hono@4.6.4)
|
||||
hono-zod-openapi:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(hono@4.6.4)(zod@3.23.8)
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0(hono@4.6.4)(zod@3.23.8)
|
||||
html-entities:
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
|
|
@ -2554,8 +2554,8 @@ packages:
|
|||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
bullmq@5.19.0:
|
||||
resolution: {integrity: sha512-S6ZxVqPgzvKVkGjUN5Qwi0bDgM2aZPKsgJ8ESe5gUOOt3APDRPfDAzrkUz1FkTd1nfgc3HFBN8MCipWDGTdFGA==}
|
||||
bullmq@5.19.1:
|
||||
resolution: {integrity: sha512-ziQ2C0ZS39MwerK+C1W88L4niDgrByVTogr8PeLHVEPhSHNCW39a/ROphNAdGWZ4M4hcXolloyAGgdtEz5Fw5A==}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
|
|
@ -3337,8 +3337,8 @@ packages:
|
|||
peerDependencies:
|
||||
hono: ^4.1.1
|
||||
|
||||
hono-zod-openapi@0.2.1:
|
||||
resolution: {integrity: sha512-KHxVkmokPzRuXWpner1Hb3Eey/FmZOn8+IMNISWhvMIdvdKOo8qbjNzTUmRMRZZYewqa1SeSy2slFCZrUNBfGw==}
|
||||
hono-zod-openapi@0.3.0:
|
||||
resolution: {integrity: sha512-Ledae1WTC+ct7Mj61y+KnrwgPm34CkU1wTt85C2bBoD60b7DR7ExJEUj77tZU7O8Y7G8p9NWwYngfbzGDTBtYA==}
|
||||
peerDependencies:
|
||||
hono: ^4.6.3
|
||||
zod: ^3.21.4
|
||||
|
|
@ -7008,7 +7008,7 @@ snapshots:
|
|||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
bullmq@5.19.0:
|
||||
bullmq@5.19.1:
|
||||
dependencies:
|
||||
cron-parser: 4.9.0
|
||||
ioredis: 5.4.1
|
||||
|
|
@ -7860,7 +7860,7 @@ snapshots:
|
|||
dependencies:
|
||||
hono: 4.6.4
|
||||
|
||||
hono-zod-openapi@0.2.1(hono@4.6.4)(zod@3.23.8):
|
||||
hono-zod-openapi@0.3.0(hono@4.6.4)(zod@3.23.8):
|
||||
dependencies:
|
||||
'@hono/zod-validator': 0.4.1(hono@4.6.4)(zod@3.23.8)
|
||||
hono: 4.6.4
|
||||
|
|
|
|||
12
src/lib/server/api/common/openapi/create-auth-route.ts
Normal file
12
src/lib/server/api/common/openapi/create-auth-route.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { type HonoOpenApiOperation, type HonoOpenApiRequestSchemas, defineOpenApiOperation } from "hono-zod-openapi";
|
||||
|
||||
export const taggedAuthRoute = <T extends HonoOpenApiRequestSchemas>(
|
||||
tag: string,
|
||||
doc: HonoOpenApiOperation<T>,
|
||||
) => {
|
||||
return defineOpenApiOperation({
|
||||
...doc,
|
||||
tags: [tag],
|
||||
security: [{ cookieAuth: [] }],
|
||||
});
|
||||
};
|
||||
|
|
@ -44,13 +44,13 @@ export default function configureOpenAPI(app: AppOpenAPI) {
|
|||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
},
|
||||
cookieAuth: {
|
||||
type: 'apiKey',
|
||||
name: 'session',
|
||||
in: 'cookie',
|
||||
}
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
app.get(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
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';
|
||||
|
|
@ -14,6 +13,7 @@ import { openApi } from 'hono-zod-openapi';
|
|||
import { setCookie } from 'hono/cookie';
|
||||
import { inject, injectable } from 'tsyringe';
|
||||
import { requireAuth } from '../middleware/require-auth.middleware';
|
||||
import { iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword } from './iam.routes';
|
||||
|
||||
@injectable()
|
||||
export class IamController extends Controller {
|
||||
|
|
@ -42,26 +42,26 @@ export class IamController extends Controller {
|
|||
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('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
return c.json({ user: updatedUser }, StatusCodes.OK);
|
||||
},
|
||||
)
|
||||
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), openApi(verifyPassword), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user;
|
||||
const { password } = c.req.valid('json');
|
||||
const passwordVerified = await this.iamService.verifyPassword(user.id, { password });
|
||||
if (!passwordVerified) {
|
||||
console.log('Incorrect password');
|
||||
return c.json('Incorrect password', StatusCodes.BAD_REQUEST);
|
||||
return c.json('Incorrect password', StatusCodes.BAD_GATEWAY);
|
||||
}
|
||||
return c.json({}, StatusCodes.OK);
|
||||
})
|
||||
.put('/update/password', requireAuth, zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
.put('/update/password', requireAuth, openApi(updatePassword), zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user;
|
||||
const { password, confirm_password } = c.req.valid('json');
|
||||
if (password !== confirm_password) {
|
||||
return c.json('Passwords do not match', StatusCodes.BAD_REQUEST);
|
||||
return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
try {
|
||||
await this.iamService.updatePassword(user.id, { password, confirm_password });
|
||||
|
|
@ -80,19 +80,19 @@ export class IamController extends Controller {
|
|||
return c.json({ status: 'success' });
|
||||
} catch (error) {
|
||||
console.error('Error updating password', error);
|
||||
return c.json('Error updating password', StatusCodes.BAD_REQUEST);
|
||||
return c.json('Error updating password', StatusCodes.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
})
|
||||
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
.post('/update/email', requireAuth, openApi(updateEmail), zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user;
|
||||
const { email } = c.req.valid('json');
|
||||
const updatedUser = await this.iamService.updateEmail(user.id, { email });
|
||||
if (!updatedUser) {
|
||||
return c.json('Email already in use', StatusCodes.BAD_REQUEST);
|
||||
return c.json('Cannot change email address', StatusCodes.BAD_REQUEST);
|
||||
}
|
||||
return c.json({ user: updatedUser }, StatusCodes.OK);
|
||||
})
|
||||
.post('/logout', requireAuth, async (c) => {
|
||||
.post('/logout', requireAuth, openApi(logout), async (c) => {
|
||||
const sessionId = c.var.session.id;
|
||||
await this.iamService.logout(sessionId);
|
||||
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { unauthorizedSchema } from '$lib/server/api/common/exceptions';
|
||||
import { createAuthCookieSchema } from '$lib/server/api/common/openapi/create-cookie-schema';
|
||||
import { selectUserSchema } from '$lib/server/api/databases/tables/users.table';
|
||||
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
|
||||
import { z } from '@hono/zod-openapi';
|
||||
import { defineOpenApiOperation } from 'hono-zod-openapi';
|
||||
import { createErrorSchema } from 'stoker/openapi/schemas';
|
||||
import { taggedAuthRoute } from '../common/openapi/create-auth-route';
|
||||
import { changePasswordDto } from '../dtos/change-password.dto';
|
||||
import { verifyPasswordDto } from '../dtos/verify-password.dto';
|
||||
|
||||
const tags = ['IAM'];
|
||||
const tag = 'IAM';
|
||||
|
||||
export const iam = defineOpenApiOperation({
|
||||
tags,
|
||||
export const iam = taggedAuthRoute(tag, {
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'User profile',
|
||||
|
|
@ -25,9 +24,7 @@ export const iam = defineOpenApiOperation({
|
|||
},
|
||||
});
|
||||
|
||||
export const updateProfile = defineOpenApiOperation({
|
||||
tags,
|
||||
security: [{ bearerAuth: [] }],
|
||||
export const updateProfile = taggedAuthRoute(tag, {
|
||||
request: {
|
||||
json: updateProfileDto,
|
||||
},
|
||||
|
|
@ -37,11 +34,104 @@ export const updateProfile = defineOpenApiOperation({
|
|||
schema: selectUserSchema,
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNPROCESSABLE_ENTITY]: {
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: 'The validation error(s)',
|
||||
schema: createErrorSchema(updateProfileDto),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNPROCESSABLE_ENTITY]: {
|
||||
description: 'Username already in use',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const verifyPassword = taggedAuthRoute(tag, {
|
||||
request: {
|
||||
json: verifyPasswordDto,
|
||||
},
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'Password verified',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: 'The validation error(s)',
|
||||
schema: createErrorSchema(verifyPasswordDto),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: 'Incorrect password',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updatePassword = taggedAuthRoute(tag, {
|
||||
request: {
|
||||
json: changePasswordDto,
|
||||
},
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'Password updated',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: 'The validation error(s)',
|
||||
schema: createErrorSchema(changePasswordDto),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: 'Incorrect password',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updateEmail = taggedAuthRoute(tag, {
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'Email updated',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: 'The validation error(s)',
|
||||
schema: createErrorSchema(changePasswordDto),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.BAD_REQUEST]: {
|
||||
description: "Cannot change email address",
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const logout = taggedAuthRoute(tag, {
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'Logged out',
|
||||
mediaType: 'application/json',
|
||||
},
|
||||
[StatusCodes.UNAUTHORIZED]: {
|
||||
description: 'Unauthorized',
|
||||
schema: createErrorSchema(unauthorizedSchema),
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { LuciaService } from '$lib/server/api/services/lucia.service'
|
|||
import { zValidator } from '@hono/zod-validator'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
import { TimeSpan } from 'oslo'
|
||||
import { openApi } from 'hono-zod-openapi';
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { limiter } from '../middleware/rate-limiter.middleware'
|
||||
import { LoginRequestsService } from '../services/loginrequest.service'
|
||||
import { signinUsername } from './login.routes'
|
||||
|
||||
@injectable()
|
||||
export class LoginController extends Controller {
|
||||
|
|
@ -19,7 +21,7 @@ export class LoginController extends Controller {
|
|||
}
|
||||
|
||||
routes() {
|
||||
return this.controller.post('/', zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
return this.controller.post('/', openApi(signinUsername), zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const { username, password } = c.req.valid('json')
|
||||
const session = await this.loginRequestsService.verify({ username, password }, c.req)
|
||||
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
|
||||
|
|
|
|||
21
src/lib/server/api/controllers/login.routes.ts
Normal file
21
src/lib/server/api/controllers/login.routes.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineOpenApiOperation } from "hono-zod-openapi";
|
||||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { signinUsernameDto } from "../dtos/signin-username.dto";
|
||||
import { createErrorSchema } from "stoker/openapi/schemas";
|
||||
|
||||
export const signinUsername = defineOpenApiOperation({
|
||||
tags: ['Login'],
|
||||
summary: 'Sign in with username',
|
||||
description: 'Sign in with username',
|
||||
responses: {
|
||||
[StatusCodes.OK]: {
|
||||
description: 'Sign in with username',
|
||||
schema: signinUsernameDto,
|
||||
},
|
||||
[StatusCodes.UNPROCESSABLE_ENTITY]: {
|
||||
description: 'The validation error(s)',
|
||||
schema: createErrorSchema(signinUsernameDto),
|
||||
mediaType: 'application/json',
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue