Changing the security auth openapi to be cookie session and documenting more APIs.

This commit is contained in:
Bradley Shellnut 2024-10-13 23:10:35 -07:00
parent eb1d44037e
commit cbcbbed912
8 changed files with 163 additions and 38 deletions

View file

@ -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",

View file

@ -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

View 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: [] }],
});
};

View file

@ -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(

View file

@ -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();

View file

@ -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),

View file

@ -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)

View 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',
}
}
});