From 964fe908a66d029b65baf4d7785ec361a0077ce4 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Sat, 30 Aug 2025 23:23:34 +0200 Subject: [PATCH] feat(auth): Base auth with plugins - Add fastify to protect routes plugins (requireAuth: true) - Simple Demo to show regular password auth (no 2FA/OTP nor remote auth) - Currently supports: login, logout, signin - OTP workflow should work, not tested - Fixed convention for docker volumes (now all placed in /volumes/) --- docker-compose.yml | 18 +++-- nginx/conf/locations/auth.conf | 7 +- src/@shared/src/auth/index.ts | 122 +++++++++++++++++++++--------- src/@shared/src/database/index.ts | 9 ++- src/@shared/src/utils/index.ts | 58 ++++++++++++++ src/Dockerfile | 1 - src/auth/entrypoint.sh | 3 +- src/auth/extra/login_demo.html | 84 ++++++++++++++++++++ src/auth/package.json | 1 - src/auth/src/app.ts | 16 +--- src/auth/src/routes/login.ts | 50 ++++-------- src/auth/src/routes/logout.ts | 12 +++ src/auth/src/routes/otp.ts | 42 ++++------ src/auth/src/routes/signin.ts | 94 ++++++++--------------- src/auth/src/routes/whoami.ts | 9 ++- src/package-lock.json | 64 +++++++++++++++- src/tsconfig.base.json | 5 +- 17 files changed, 398 insertions(+), 197 deletions(-) create mode 100644 src/@shared/src/utils/index.ts create mode 100644 src/auth/extra/login_demo.html create mode 100644 src/auth/src/routes/logout.ts diff --git a/docker-compose.yml b/docker-compose.yml index ba7e23c..8b18715 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,8 +18,8 @@ services: - '8888:443' volumes: # if you need to share files with nginx, you do it here. + - static-volume:/volumes/static - images-volume:/volumes/icons - # - static-volume:/volumes/static environment: # this can stay the same for developpement. This is an alias to `localhost` - NGINX_DOMAIN=local.maix.me @@ -38,11 +38,11 @@ services: networks: - transcendance-network volumes: - - images-volume:/store - - sqlite-volume:/database + - images-volume:/volumes/store + - sqlite-volume:/volumes/database environment: - - USER_ICONS_STORE=/store - - DATABASE_DIR=/database + - USER_ICONS_STORE=/volumes/store + - DATABASE_DIR=/volumes/database ############### @@ -53,15 +53,17 @@ services: context: ./src/ args: - SERVICE=auth - #- EXTRA_FILES=icons/extra + - EXTRA_FILES=auth/extra container_name: auth restart: always networks: - transcendance-network volumes: - - sqlite-volume:/database + - sqlite-volume:/volumes/database + - static-volume:/volumes/static environment: - - DATABASE_DIR=/database + - JWT_SECRET=KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JA + - DATABASE_DIR=/volumes/database volumes: diff --git a/nginx/conf/locations/auth.conf b/nginx/conf/locations/auth.conf index 9ff9048..0472062 100644 --- a/nginx/conf/locations/auth.conf +++ b/nginx/conf/locations/auth.conf @@ -1,5 +1,8 @@ #forward the post request to the microservice location /api/auth/ { - rewrite ^/api/auth/(.*) $1 break; - proxy_pass http://auth/$uri; + proxy_pass http://auth$uri; +} + +location / { + root /volumes/static/auth/; } diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts index b78ca38..f79835e 100644 --- a/src/@shared/src/auth/index.ts +++ b/src/@shared/src/auth/index.ts @@ -1,67 +1,119 @@ import OTP from "otp"; -import fastifyJwt from "@fastify/jwt"; -import fp from 'fastify-plugin' -import { FastifyPluginAsync, FastifyRequest } from "fastify"; -import { Static, Type } from "@sinclair/typebox" -import { useDatabase, user } from "@shared/database" import cookie from "@fastify/cookie"; +import fastifyJwt from "@fastify/jwt"; +import fp from "fastify-plugin"; +import { FastifyPluginAsync, preValidationAsyncHookHandler } from "fastify"; +import { Static, Type } from "@sinclair/typebox"; +import { UserId } from "@shared/database/mixin/user"; +import { useDatabase } from "@shared/database"; +import { makeResponse } from "@shared/utils"; +const kRouteAuthDone = Symbol("shared-route-auth-done"); -const kRouteAuthDone = Symbol('shared-route-auth-done'); +type AuthedUser = { + id: UserId; + name: string; +}; -declare module 'fastify' { +declare module "fastify" { export interface FastifyInstance { signJwt: (kind: "auth" | "otp", who: string) => string; + [s: symbol]: boolean; } export interface FastifyRequest { - authUser?: user.UserId; + authUser?: AuthedUser; } export interface FastifyContextConfig { - requireAuth?: boolean, + requireAuth?: boolean; } } export const Otp = OTP; +let jwtAdded = false; export const jwtPlugin = fp(async (fastify, _opts) => { + if (jwtAdded) jwtAdded = true; let env = process.env.JWT_SECRET; - if (env === undefined || env === null) - throw "JWT_SECRET is not defined" - void fastify.register(fastifyJwt, { - secret: env, - decode: { complete: false }, - }); - void fastify.decorate("signJwt", (kind, who) => fastify.jwt.sign({ kind, who, createdAt: Date.now() })) + if (env === undefined || env === null) throw "JWT_SECRET is not defined"; + if (!fastify.hasDecorator("signJwt")) { + void fastify.decorate("signJwt", (kind, who) => + fastify.jwt.sign({ kind, who, createdAt: Date.now() }), + ); + void fastify.register(fastifyJwt, { + secret: env, + decode: { complete: false }, + }); + } }); export const JwtType = Type.Object({ kind: Type.Union([ - Type.Const("otp", { description: "the token is only valid for otp call" }), - Type.Const("auth", { description: "the token is valid for authentication" }) + Type.Const("otp", { + description: "the token is only valid for otp call", + }), + Type.Const("auth", { + description: "the token is valid for authentication", + }), ]), who: Type.String({ description: "the login of the user" }), - createdAt: Type.Integer({ description: "Unix timestamp of when the token as been created at" }) + createdAt: Type.Integer({ + description: "Unix timestamp of when the token as been created at", + }), }); export type JwtType = Static; - +let authAdded = false; export const authPlugin = fp(async (fastify, _opts) => { + if (authAdded) return void console.log("skipping"); + authAdded = true; await fastify.register(useDatabase as any, {}); await fastify.register(jwtPlugin as any, {}); await fastify.register(cookie); - fastify.addHook('onRoute', (routeOpts) => { - if (routeOpts.config?.requireAuth) { - routeOpts.preValidation = [function(req, res) { - if (req.cookies.token === undefined) - return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" }) - let tok = this.jwt.verify(req.cookies.token); - if (tok.kind != "auth") - return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" }) - let user = this.db.getUserFromName(tok.who); - if (user === null) - return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" }) - req.authUser = user.id; - }, ...(routeOpts.preValidation as any || []),]; + if (!fastify.hasRequestDecorator("authUser")) + fastify.decorateRequest("authUser", undefined); + fastify.addHook("onRoute", (routeOpts) => { + if ( + routeOpts.config?.requireAuth && + !(routeOpts as any)[kRouteAuthDone] + ) { + let f: preValidationAsyncHookHandler = async function(req, res) { + try { + if (req.cookies.token === undefined) + return res + .clearCookie("token") + .send( + JSON.stringify(makeResponse("notLoggedIn", "auth.noCookie")), + ); + let tok = this.jwt.verify(req.cookies.token); + if (tok.kind != "auth") + return res + .clearCookie("token") + .send( + JSON.stringify(makeResponse("notLoggedIn", "auth.invalidKind")), + ); + let user = this.db.getUserFromName(tok.who); + if (user === null) + return res + .clearCookie("token") + .send( + JSON.stringify(makeResponse("notLoggedIn", "auth.noUser")), + ); + req.authUser = { id: user.id, name: tok.who }; + } catch { + return res + .clearCookie("token") + .send(JSON.stringify(makeResponse("notLoggedIn", "auth.invalid"))); + } + }; + if (!routeOpts.preValidation) { + routeOpts.preValidation = [f]; + } else if (Array.isArray(routeOpts.preValidation)) { + routeOpts.preValidation.push(f); + } else { + routeOpts.preValidation = [routeOpts.preValidation, f]; + } + + (routeOpts as any)[kRouteAuthDone] = true; } - }) -}) + }); +}); diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index 2382a70..d684f7c 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -16,16 +16,21 @@ declare module 'fastify' { } } +let dbAdded = false; + export const useDatabase = fp(async function( f: FastifyInstance, _options: {}) { - + if (dbAdded) + return; + dbAdded = true; let path = process.env.DATABASE_DIR; if (path === null || path === undefined) throw "env `DATABASE_DIR` not defined"; f.log.info(`Opening database with path: ${path}/database.db`) let db: Database = new DbImpl(`${path}/database.db`) as Database; - f.decorate('db', db); + if (!f.hasDecorator("db")) + f.decorate('db', db); }); export * as user from "./mixin/user" diff --git a/src/@shared/src/utils/index.ts b/src/@shared/src/utils/index.ts new file mode 100644 index 0000000..b41b1df --- /dev/null +++ b/src/@shared/src/utils/index.ts @@ -0,0 +1,58 @@ +import { Type } from "@sinclair/typebox"; + +/** + * @description Represent a message key + * Used for translation of text, taken from a prebuilt dictionary + * Format: `category.sub.desc` + * + * @example `login.failure.invalid` + * @example `login.failure.missingPassword` + * @example `login.failure.missingUser` + * @example `signin.success` + * @example `pong.you.lost` + */ +export type MessageKey = string; +export type ResponseBase = { + kind: string, + msg: MessageKey, + payload?: T, +} + +/** + * @description Builds a response from a `kind`, `key` and an arbitrary payload + * + * * USE THIS FUNCTION TO ALLOW GREPING :) * + * + * @example makeResponse("failure", "login.failure.invalid") + * @example makeResponse("success", "login.success", { token: "supersecrettoken" }) + */ +export function makeResponse(kind: string, key: MessageKey, payload?: T): ResponseBase { + console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`) + return { kind, msg: key, payload } +} + + +/** + * @description Create a typebox Type for a response. + * + * @example typeResponse("failure", ["login.failure.invalid", "login.failure.generic", "login.failure.missingPassword"]) + * @example typeResponse("otpRequired", "login.otpRequired", { token: Type.String() }) + * @example typeResponse("success", "login.success", { token: Type.String() }) + */ +export function typeResponse(kind: string, key: MessageKey | MessageKey[], payload?: any): any { + let tKey; + if (key instanceof Array) { + tKey = Type.Union(key.map(l => Type.Const(l))); + } else { + tKey = Type.Const(key); + } + + const Ty = { + kind: Type.Const(kind), + msg: tKey, + }; + if (payload !== undefined) + Object.assign(Ty, { payload: Type.Object(payload) }) + + return Type.Object(Ty) +} diff --git a/src/Dockerfile b/src/Dockerfile index bc7d4e0..be9dc06 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -36,7 +36,6 @@ ARG EXTRA_FILES=empty COPY --from=builder /dist /src RUN pnpm install --prod --frozen-lockfile; COPY ${EXTRA_FILES} /extra -RUN echo "${EXTRA_FILES}"; ENTRYPOINT [ "/src/entrypoint.sh" ] CMD ["node", "/src/run.cjs"] diff --git a/src/auth/entrypoint.sh b/src/auth/entrypoint.sh index 4a3dac4..45ad91f 100644 --- a/src/auth/entrypoint.sh +++ b/src/auth/entrypoint.sh @@ -4,7 +4,8 @@ set -e set -x # do anything here -cp -r /extra /files +mkdir -p /volumes/static/auth/ +cp -r /extra/login_demo.html /volumes/static/auth/index.html # run the CMD [ ... ] from the dockerfile exec "$@" diff --git a/src/auth/extra/login_demo.html b/src/auth/extra/login_demo.html new file mode 100644 index 0000000..e731d1d --- /dev/null +++ b/src/auth/extra/login_demo.html @@ -0,0 +1,84 @@ + + + Demo Page For Login :) + + + +

Welcome

+ + + +
+
+ + +
+ +
+ +
+ + +
+
+ + + + diff --git a/src/auth/package.json b/src/auth/package.json index a93831b..3d01f7c 100644 --- a/src/auth/package.json +++ b/src/auth/package.json @@ -26,7 +26,6 @@ "fastify": "^5.0.0", "fastify-cli": "^7.4.0", "fastify-plugin": "^5.0.0", - "raw-body": "^3.0.0", "sharp": "^0.34.2" }, "devDependencies": { diff --git a/src/auth/src/app.ts b/src/auth/src/app.ts index bda8745..0aba7d4 100644 --- a/src/auth/src/app.ts +++ b/src/auth/src/app.ts @@ -23,6 +23,10 @@ const app: FastifyPluginAsync = async ( fastify, opts ): Promise => { + await fastify.register(db.useDatabase as any, {}) + await fastify.register(auth.jwtPlugin as any, {}) + await fastify.register(auth.authPlugin as any, {}) + // Place here your custom code! for (const plugin of Object.values(plugins)) { void fastify.register(plugin as any, {}); @@ -31,20 +35,8 @@ const app: FastifyPluginAsync = async ( void fastify.register(route as any, {}); } - await fastify.register(db.useDatabase as any, {}) - await fastify.register(auth.jwtPlugin as any, {}) void fastify.register(fastifyFormBody, {}) void fastify.register(fastifyMultipart, {}) - console.log(fastify.db.getUser(0 as any)); - - // The use of fastify-plugin is required to be able - // to export the decorators to the outer scope - void fastify.register(fp(async (fastify) => { - const image_store = process.env.USER_ICONS_STORE ?? "/tmp/icons"; - fastify.decorate('image_store', image_store) - await mkdir(fastify.image_store, { recursive: true }) - })) - } export default app diff --git a/src/auth/src/routes/login.ts b/src/auth/src/routes/login.ts index cb035d0..3ca8448 100644 --- a/src/auth/src/routes/login.ts +++ b/src/auth/src/routes/login.ts @@ -3,74 +3,56 @@ import { FastifyPluginAsync } from "fastify"; import { Static, Type } from "@sinclair/typebox"; import { user as userDb } from "@shared/database"; import type { } from "@shared/auth"; +import { typeResponse, makeResponse } from "@shared/utils" export const LoginReq = Type.Object({ name: Type.String(), - password: Type.String({ minLength: 8, maxLength: 32 }), + password: Type.String(), }); export type LoginReq = Static; - export const LoginRes = Type.Union([ - Type.Object({ - kind: Type.Const("failed"), - msg_key: Type.Union([ - Type.Const("login.failed.generic"), - Type.Const("login.failed.invalid"), - ]), - }), - Type.Object({ - kind: Type.Const("otpRequired"), - msg_key: Type.Const("login.otpRequired"), - token: Type.String({ - description: "Code to send with the OTP to finish login", - }), - }), - Type.Object({ - kind: Type.Const("success"), - msg_key: Type.Const("login.success"), - token: Type.String({ description: "The JWT token" }), - }), + typeResponse("failed", ["login.failed.generic", "login.failed.invalid"]), + typeResponse("otpRequired", "login.otpRequired", { token: Type.String({ description: "JWT to send with the OTP to finish login" }) }), + typeResponse("success", "login.success", { token: Type.String({ description: "JWT that represent a logged in user" }) }), ]); + export type LoginRes = Static; const route: FastifyPluginAsync = async (fastify, opts): Promise => { fastify.post<{ Body: LoginReq; Response: LoginRes }>( - "/login", - { - schema: { - body: LoginReq, - response: { "2xx": LoginRes }, - }, - }, + "/api/auth/login", + { schema: { body: LoginReq, response: { "2xx": LoginRes } }, }, async function(req, res) { try { let { name, password } = req.body; + console.log("HELLOO FDP"); let user = this.db.getUserFromName(name); // does the user exist // does it have a password setup ? if (user === null || user.password === null) - return { kind: "failed", msg_key: "login.failed.invalid" }; + return makeResponse("failed", "login.failed.invalid"); // does the password he provided match the one we have if (!(await userDb.verifyUserPassword(user, password))) - return { kind: "failed", msg_key: "login.failed.invalid" }; + return makeResponse("failed", "login.failed.invalid"); // does the user has 2FA up ? - if (user.otp !== null) { + if (user.otp !== undefined) { + console.log(user); // yes -> we ask them to fill it, // send them somehting to verify that they indeed passed throught the user+password phase - return { kind: "otpRequired", msg_key: "login.otpRequired", token: this.signJwt("otp", user.name) }; + return makeResponse("otpRequired", "login.otpRequired", { token: this.signJwt("otp", user.name) }); } // every check has been passed, they are now logged in, using this token to say who they are... - return { kind: "success", msg_key: "login.success", token: this.signJwt("auth", user.name) } + return makeResponse("success", "login.success", { token: this.signJwt("auth", user.name) }); } catch { - return { kind: "failed", msg_key: "login.failed.generic" }; + return makeResponse("failed", "login.failed.generic"); } }, ); diff --git a/src/auth/src/routes/logout.ts b/src/auth/src/routes/logout.ts new file mode 100644 index 0000000..3acd1d9 --- /dev/null +++ b/src/auth/src/routes/logout.ts @@ -0,0 +1,12 @@ +import { FastifyPluginAsync } from "fastify"; + +const route: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.post( + "/api/auth/logout", + async function(req, res) { + return res.clearCookie("token").send("bye :(") + }, + ); +}; + +export default route; diff --git a/src/auth/src/routes/otp.ts b/src/auth/src/routes/otp.ts index 2be2e6d..e542fda 100644 --- a/src/auth/src/routes/otp.ts +++ b/src/auth/src/routes/otp.ts @@ -2,37 +2,27 @@ import { FastifyPluginAsync } from "fastify"; import { Static, Type } from "@sinclair/typebox"; import { JwtType, Otp } from "@shared/auth"; +import { typeResponse, makeResponse } from "@shared/utils"; -export const OtpReq = Type.Object({ +const OtpReq = Type.Object({ token: Type.String({ description: "The token given at the login phase" }), code: Type.String({ description: "The OTP given by the user" }), }); -export type OtpReq = Static; +type OtpReq = Static; -export const OtpRes = Type.Union([ - Type.Object({ - kind: Type.Const("failed"), - msg_key: Type.Union([ - Type.Const("otp.failed.generic"), - Type.Const("otp.failed.invalid"), - Type.Const("otp.failed.timeout"), - ]), - }), - Type.Object({ - kind: Type.Const("success"), - msg_key: Type.Const("otp.success"), - token: Type.String({ description: "The JWT token" }), - }), +const OtpRes = Type.Union([ + typeResponse("failed", ["otp.failed.generic", "otp.failed.invalid", "otp.failed.timeout"]), + typeResponse("success", "otp.success", { token: Type.String({ description: "the JWT Token" }) }), ]); -export type OtpRes = Static; +type OtpRes = Static; const OTP_TOKEN_TIMEOUT_SEC = 120; const route: FastifyPluginAsync = async (fastify, opts): Promise => { - fastify.get<{ Body: OtpReq }>( - "/whoami", + fastify.post<{ Body: OtpReq }>( + "/api/auth/otp", { schema: { body: OtpReq, response: { "2xx": OtpRes } } }, async function(req, res) { try { @@ -43,18 +33,18 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise => { // is the jwt a valid `otp` jwt ? if (dJwt.kind != "otp") // no ? fuck off then - return { kind: "failed", msg_key: "otp.failed.invalid" }; + return makeResponse("failed", "otp.failed.invalid"); // is it too old ? if (dJwt.createdAt + OTP_TOKEN_TIMEOUT_SEC * 1000 > Date.now()) // yes ? fuck off then, redo the password - return { kind: "failed", msg_key: "otp.failed.timeout" }; + return makeResponse("failed", "otp.failed.timeout"); // get the Otp sercret from the db let otpSecret = this.db.getUserFromName(dJwt.who)?.otp; if (otpSecret === null) // oops, either no user, or user without otpSecret // fuck off - return { kind: "failed", msg_key: "otp.failed.invalid" }; + return makeResponse("failed", "otp.failed.invalid"); // good lets now verify the token you gave us is the correct one... let otpHandle = new Otp({ secret: otpSecret }); @@ -71,13 +61,9 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise => { if (tokens.some((c) => c === code)) // they do ! // gg you are now logged in ! - return { - kind: "success", - msg_key: "otp.success", - token: this.signJwt("auth", dJwt.who), - }; + return makeResponse("success", "otp.success", { token: this.signJwt("auth", dJwt.who) }); } catch { - return { kind: "failed", msg_key: "otp.failed.generic" }; + return makeResponse("failed", "otp.failed.generic"); } }, ); diff --git a/src/auth/src/routes/signin.ts b/src/auth/src/routes/signin.ts index ea7c41e..38a1914 100644 --- a/src/auth/src/routes/signin.ts +++ b/src/auth/src/routes/signin.ts @@ -1,97 +1,63 @@ import { FastifyPluginAsync } from "fastify"; import { Static, Type } from "@sinclair/typebox"; +import { typeResponse, makeResponse } from "@shared/utils"; const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/; -export const SignInReq = Type.Object({ +const SignInReq = Type.Object({ name: Type.String(), - password: Type.String({ minLength: 8, maxLength: 32 }), + password: Type.String(), }); -export type SignInReq = Static; +type SignInReq = Static; -export const SignInRes = Type.Union([ - Type.Object({ - kind: Type.Const("failed"), - msg_key: Type.Union([ - Type.Const("signin.failed.generic"), - Type.Const("signin.failed.username.existing"), - Type.Const("signin.failed.username.toolong"), - Type.Const("signin.failed.username.tooshort"), - Type.Const("signin.failed.username.invalid"), - Type.Const("signin.failed.password.toolong"), - Type.Const("signin.failed.password.tooshort"), - Type.Const("signin.failed.password.invalid"), - ]), - }), - Type.Object({ - kind: Type.Const("sucess"), - msg_key: Type.Const("signin.sucess"), - token: Type.String({ description: "The JWT token" }), - }), -]); +const SignInRes = Type.Union([ + typeResponse("failed", [ + "signin.failed.generic", + "signin.failed.username.existing", + "signin.failed.username.toolong", + "signin.failed.username.tooshort", + "signin.failed.username.invalid", + "signin.failed.password.toolong", + "signin.failed.password.tooshort", + "signin.failed.password.invalid", + ]), + typeResponse("success", "signin.success", { token: Type.String({ description: "the JWT token" }) }), +]) -export type SignInRes = Static; +type SignInRes = Static; const route: FastifyPluginAsync = async (fastify, opts): Promise => { - fastify.post<{ Body: SignInReq; Response: SignInRes }>( - "/signin", - { schema: { body: SignInReq, response: { "2xx": SignInRes } } }, + fastify.post<{ Body: SignInReq }>( + "/api/auth/signin", + { schema: { body: SignInReq, response: { "200": SignInRes, "5xx": Type.Object({}) } }, }, async function(req, res) { const { name, password } = req.body; if (name.length < 4) - return { - kind: "failed", - msg_key: "signin.failed.username.tooshort", - }; + return makeResponse("failed", "signin.failed.username.tooshort"); if (name.length > 32) - return { - kind: "failed", - msg_key: "signin.failed.username.toolong", - }; + return makeResponse("failed", "signin.failed.username.toolong"); if (!USERNAME_CHECK.test(name)) - return { - kind: "failed", - msg_key: "signin.failed.username.invalid", - }; + return makeResponse("failed", "signin.failed.username.invalid"); // username if good now :) if (password.length < 8) - return { - kind: "failed", - msg_key: "signin.failed.password.tooshort", - }; + return makeResponse("failed", "signin.failed.password.tooshort"); if (password.length > 64) - return { - kind: "failed", - msg_key: "signin.failed.password.toolong", - }; + return makeResponse("failed", "signin.failed.password.toolong"); // password is good too ! if (this.db.getUserFromName(name) !== null) - return { - kind: "failed", - msg_key: "signin.failed.username.existing", - }; + return makeResponse("failed", "signin.failed.username.existing"); let u = await this.db.createUser(name, password); if (u === null) - return { kind: "failed", msg_key: "signin.failed.generic" }; + return makeResponse("failed", "signin.failed.generic"); // every check has been passed, they are now logged in, using this token to say who they are... - let userToken = this.jwt.sign({ - kind: "auth", - user: u.name, - createAt: Date.now() / 1000, - }); - let out = { - kind: "success", - msg_key: "login.success", - token: userToken, - }; - console.log(out) - return out; + let userToken = this.signJwt('auth', u.name); + return makeResponse("success", "signin.success", { token: userToken }); }, ); }; diff --git a/src/auth/src/routes/whoami.ts b/src/auth/src/routes/whoami.ts index 54d19a7..8200541 100644 --- a/src/auth/src/routes/whoami.ts +++ b/src/auth/src/routes/whoami.ts @@ -1,18 +1,19 @@ import { FastifyPluginAsync } from "fastify"; import { Static, Type } from "@sinclair/typebox"; +import { makeResponse, typeResponse } from "@shared/utils" -export const WhoAmIRes = Type.String({ description: "username" }); +export const WhoAmIRes = typeResponse("success", "whoami.success", { name: Type.String() }); export type WhoAmIRes = Static; const route: FastifyPluginAsync = async (fastify, opts): Promise => { fastify.get( - "/whoami", - { schema: { response: { "2xx": WhoAmIRes } } }, + "/api/auth/whoami", + { schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } }, async function(req, res) { - return "yes"; + return makeResponse("success", "whoami.success", { name: req.authUser?.name }) }, ); }; diff --git a/src/package-lock.json b/src/package-lock.json index 84d73a8..dd33633 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -24,13 +24,16 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", "@fastify/jwt": "^9.1.0", + "@sinclair/typebox": "^0.34.40", "@types/bcrypt": "^6.0.0", "bcrypt": "^6.0.0", "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.1", "joi": "^18.0.0", + "otp": "^1.1.2", "uuidv7": "^1.0.2" }, "devDependencies": { @@ -51,7 +54,6 @@ "fastify": "^5.0.0", "fastify-cli": "^7.4.0", "fastify-plugin": "^5.0.0", - "raw-body": "^3.0.0", "sharp": "^0.34.2" }, "devDependencies": { @@ -594,6 +596,26 @@ "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", "license": "MIT" }, + "node_modules/@fastify/cookie": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz", + "integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, "node_modules/@fastify/deepmerge": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.2.tgz", @@ -1997,6 +2019,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2112,6 +2143,15 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -3118,6 +3158,15 @@ "wrappy": "1" } }, + "node_modules/otp": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/otp/-/otp-1.1.2.tgz", + "integrity": "sha512-VoueTSCMNTCYyHMGkNfndkFSXNv+iyEJ8D1/zD5G0Rd/QUHWozySmezRWKdVAhxcvmL3e5qwhEJBH/JF9MyE+g==", + "license": "MIT", + "dependencies": { + "sha1": "^1.1.1" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3691,6 +3740,19 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/shared": { "resolved": "@shared", "link": true diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index e0ab587..dd0ae28 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -22,10 +22,7 @@ "skipLibCheck": true, "lib": ["ESNext"], "paths": { - "@shared/auth": ["@shared/src/auth"], - "@shared/auth/*": ["@shared/src/auth/*"], - "@shared/database": ["@shared/src/database"], - "@shared/database/*": ["@shared/src/database/*"] + "@shared/*": ["@shared/src/*"] } } }