From 874b4804b95d24abd6c836d1832544713cce8a7a Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Tue, 4 Jun 2024 21:28:38 +0300 Subject: [PATCH] Docker --- .dockerignore | 4 ++ Dockerfile | 29 ++++++++ src/hooks.server.ts | 8 ++- src/lib/server/drizzle/index.ts | 22 ++++-- src/lib/server/drizzle/seeds/privileges.ts | 6 +- src/lib/server/jwt.ts | 31 +++++--- src/lib/server/oauth2/model/client.ts | 42 +++++------ src/lib/server/oauth2/model/tokens.ts | 20 +++--- src/lib/server/oauth2/model/user.ts | 14 ++-- src/lib/server/upload.ts | 70 ++++++++++++++----- src/lib/server/users/admin.ts | 10 +-- src/lib/server/users/index.ts | 40 +++++------ src/lib/server/users/tokens.ts | 12 ++-- src/lib/server/users/totp.ts | 8 +-- .../jwks.json/+server.ts | 7 +- src/routes/api/avatar/[uuid]/+server.ts | 3 +- .../api/avatar/client/[uuid]/+server.ts | 3 +- 17 files changed, 211 insertions(+), 118 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c5292ac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +uploads +node_modules +private +devdocker diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8c2ef63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build the application with a specific environment +FROM node:20 AS builder + +WORKDIR /usr/src/app + +ARG envFile=.env + +COPY . . +COPY ./${envFile} ./.env + +RUN npm ci +RUN npm run build + +# Create the executor image +FROM node:20 + +WORKDIR /app + +COPY --from=builder --chown=node:node /usr/src/app/build ./build +COPY --from=builder --chown=node:node /usr/src/app/migrations ./migrations +COPY --from=builder --chown=node:node /usr/src/app/package* . +RUN npm ci --omit=dev + +USER node + +VOLUME [ "/app/private" ] +VOLUME [ "/app/uploads" ] + +CMD [ "node", "/app/build" ] diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 64d0068..9b1082f 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,11 +1,15 @@ import { AUTO_MIGRATE, SESSION_SECRET } from '$env/static/private'; -import { db } from '$lib/server/drizzle'; +import { DB } from '$lib/server/drizzle'; import { runSeeds } from '$lib/server/drizzle/seeds'; +import { JWT } from '$lib/server/jwt'; import { migrate } from 'drizzle-orm/mysql2/migrator'; import { handleSession } from 'svelte-kit-cookie-session'; +await DB.init(); +await JWT.init(); + if (AUTO_MIGRATE === 'true') { - await migrate(db, { migrationsFolder: './migrations' }); + await migrate(DB.drizzle, { migrationsFolder: './migrations' }); } await runSeeds(); diff --git a/src/lib/server/drizzle/index.ts b/src/lib/server/drizzle/index.ts index a1edfe4..a5fa111 100644 --- a/src/lib/server/drizzle/index.ts +++ b/src/lib/server/drizzle/index.ts @@ -3,12 +3,20 @@ import { drizzle } from 'drizzle-orm/mysql2'; import mysql from 'mysql2/promise'; import * as schema from './schema'; -const connection = await mysql.createConnection({ - host: DATABASE_HOST, - user: DATABASE_PASS, - password: DATABASE_PASS, - database: DATABASE_DB -}); +export class DB { + static mysqlConnection: mysql.Connection; + static drizzle: ReturnType>; + + static async init() { + DB.mysqlConnection = await mysql.createConnection({ + host: DATABASE_HOST, + user: DATABASE_PASS, + password: DATABASE_PASS, + database: DATABASE_DB + }); + + DB.drizzle = drizzle(DB.mysqlConnection, { schema, mode: 'default' }); + } +} -export const db = drizzle(connection, { schema, mode: 'default' }); export * from './schema'; diff --git a/src/lib/server/drizzle/seeds/privileges.ts b/src/lib/server/drizzle/seeds/privileges.ts index c23a4eb..6a1aeeb 100644 --- a/src/lib/server/drizzle/seeds/privileges.ts +++ b/src/lib/server/drizzle/seeds/privileges.ts @@ -1,5 +1,5 @@ import { eq } from 'drizzle-orm'; -import { db, privilege } from '..'; +import { DB, privilege } from '..'; /** * System privileges which must always exist in the database. @@ -18,11 +18,11 @@ const privileges = [ export default async function privilegesSeed() { for (const priv of privileges) { - const [exists] = await db + const [exists] = await DB.drizzle .select({ id: privilege.id }) .from(privilege) .where(eq(privilege.name, priv)); if (exists) continue; - await db.insert(privilege).values({ name: priv }); + await DB.drizzle.insert(privilege).values({ name: priv }); } } diff --git a/src/lib/server/jwt.ts b/src/lib/server/jwt.ts index 85764be..2154c4c 100644 --- a/src/lib/server/jwt.ts +++ b/src/lib/server/jwt.ts @@ -1,11 +1,15 @@ import { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } from '$env/static/private'; import { readFile } from 'fs/promises'; -import { SignJWT, importPKCS8, importSPKI, jwtVerify } from 'jose'; - -const privateKeyFile = await readFile('private/jwt.private.pem', { encoding: 'utf-8' }); -const publicKeyFile = await readFile('private/jwt.public.pem', { encoding: 'utf-8' }); -const privateKey = await importPKCS8(privateKeyFile, JWT_ALGORITHM); -const publicKey = await importSPKI(publicKeyFile, JWT_ALGORITHM); +import { + SignJWT, + exportJWK, + importPKCS8, + importSPKI, + jwtVerify, + type JWK, + type KeyLike +} from 'jose'; +import { v4 as uuidv4 } from 'uuid'; /** * Generate JWTs using the following commands: @@ -13,8 +17,19 @@ const publicKey = await importSPKI(publicKeyFile, JWT_ALGORITHM); * Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem */ export class JWT { - static privateKey = privateKey; - static publicKey = publicKey; + static privateKey: KeyLike; + static publicKey: KeyLike; + static jwks: JWK; + static jwksKid: string; + + static async init() { + const privateKeyFile = await readFile('private/jwt.private.pem', { encoding: 'utf-8' }); + const publicKeyFile = await readFile('private/jwt.public.pem', { encoding: 'utf-8' }); + JWT.privateKey = await importPKCS8(privateKeyFile, JWT_ALGORITHM); + JWT.publicKey = await importSPKI(publicKeyFile, JWT_ALGORITHM); + JWT.jwks = await exportJWK(JWT.publicKey); + JWT.jwksKid = uuidv4({ random: Buffer.from(JWT.jwks.n as string).subarray(0, 16) }); + } static async issue(claims: Record, subject: string, audience?: string) { const sign = new SignJWT(claims) diff --git a/src/lib/server/oauth2/model/client.ts b/src/lib/server/oauth2/model/client.ts index bb02a91..36a2a2d 100644 --- a/src/lib/server/oauth2/model/client.ts +++ b/src/lib/server/oauth2/model/client.ts @@ -1,7 +1,7 @@ import { PUBLIC_URL, PUBLIC_SITE_NAME } from '$env/static/public'; import { CryptoUtils } from '$lib/server/crypto-utils'; import { - db, + DB, oauth2Client, oauth2ClientAuthorization, oauth2ClientManager, @@ -82,7 +82,7 @@ export class OAuth2Clients { public static availableUrlTypes: OAuth2ClientURLType[] = Object.values(OAuth2ClientURLType); static async fetchById(id: string | number) { - const [client] = await db + const [client] = await DB.drizzle .select() .from(oauth2Client) .where(typeof id === 'string' ? eq(oauth2Client.client_id, id) : eq(oauth2Client.id, id)) @@ -94,7 +94,7 @@ export class OAuth2Clients { id: string, type: OAuth2ClientURLType = OAuth2ClientURLType.REDIRECT_URI ) { - return await db + return await DB.drizzle .select() .from(oauth2ClientUrl) .innerJoin(oauth2Client, eq(oauth2ClientUrl.clientId, oauth2Client.id)) @@ -102,7 +102,7 @@ export class OAuth2Clients { } static async getClientUrls(client: OAuth2Client) { - return await db + return await DB.drizzle .select() .from(oauth2ClientUrl) .where(and(eq(oauth2ClientUrl.clientId, client.id))); @@ -110,7 +110,7 @@ export class OAuth2Clients { static async checkRedirectUri(client: OAuth2Client, url: string) { return !!( - await db + await DB.drizzle .select() .from(oauth2ClientUrl) .innerJoin(oauth2Client, eq(oauth2ClientUrl.clientId, oauth2Client.id)) @@ -125,7 +125,7 @@ export class OAuth2Clients { } static async getAuthorizedUsers(client: OAuth2Client, userUuid?: string) { - const junkList = await db + const junkList = await DB.drizzle .select() .from(oauth2ClientAuthorization) .innerJoin(user, eq(user.id, oauth2ClientAuthorization.userId)) @@ -215,7 +215,7 @@ export class OAuth2Clients { ) { const filterText = `%${filters?.filter?.toLowerCase()}%`; const limit = filters?.limit || 20; - const allowedClients = db + const allowedClients = DB.drizzle .select({ id: oauth2Client.id }) .from(oauth2Client) .leftJoin(oauth2ClientManager, eq(oauth2ClientManager.clientId, oauth2Client.id)) @@ -238,14 +238,14 @@ export class OAuth2Clients { .offset(filters?.offset || 0) .as('allowedClients'); - const [{ rowCount }] = await db + const [{ rowCount }] = await DB.drizzle .select({ rowCount: count(oauth2Client.id).mapWith(Number) }) .from(allowedClients) .innerJoin(oauth2Client, eq(allowedClients.id, oauth2Client.id)); - const junkList = await db + const junkList = await DB.drizzle .select({ o_auth2_client: oauth2Client, o_auth2_client_url: oauth2ClientUrl, @@ -308,7 +308,7 @@ export class OAuth2Clients { const uid = CryptoUtils.createUUID(); const secret = CryptoUtils.generateSecret(); - const [retval] = await db.insert(oauth2Client).values({ + const [retval] = await DB.drizzle.insert(oauth2Client).values({ title, description, client_id: uid, @@ -321,7 +321,7 @@ export class OAuth2Clients { verified: 0 }); - await db.insert(oauth2ClientUrl).values({ + await DB.drizzle.insert(oauth2ClientUrl).values({ type: 'redirect_uri', url: redirect, clientId: retval.insertId @@ -334,18 +334,18 @@ export class OAuth2Clients { if (client.pictureId) { await Uploads.removeClientAvatar(client); } - await db.delete(privilege).where(eq(privilege.clientId, client.id)); - await db.delete(oauth2Client).where(eq(oauth2Client.id, client.id)); + await DB.drizzle.delete(privilege).where(eq(privilege.clientId, client.id)); + await DB.drizzle.delete(oauth2Client).where(eq(oauth2Client.id, client.id)); } static async deleteUrl(client: OAuth2Client, urlId: number) { - await db + await DB.drizzle .delete(oauth2ClientUrl) .where(and(eq(oauth2ClientUrl.clientId, client.id), eq(oauth2ClientUrl.id, urlId))); } static async addUrl(client: OAuth2Client, type: OAuth2ClientURLType, url: string) { - await db.insert(oauth2ClientUrl).values({ + await DB.drizzle.insert(oauth2ClientUrl).values({ type, url, clientId: client.id @@ -353,25 +353,25 @@ export class OAuth2Clients { } static async deletePrivilege(client: OAuth2Client, privilegeId: number) { - await db + await DB.drizzle .delete(privilege) .where(and(eq(privilege.clientId, client.id), eq(privilege.id, privilegeId))); } static async addPrivilege(client: OAuth2Client, name: string) { const realName = `${client.client_id.split('-')[0]}:${name}`; - await db.insert(privilege).values({ + await DB.drizzle.insert(privilege).values({ name: realName, clientId: client.id }); } static async update(client: OAuth2Client, body: Partial) { - await db.update(oauth2Client).set(body).where(eq(oauth2Client.id, client.id)); + await DB.drizzle.update(oauth2Client).set(body).where(eq(oauth2Client.id, client.id)); } static async getManagers(client: OAuth2Client) { - return await db + return await DB.drizzle .select({ id: oauth2ClientManager.id, email: user.email }) .from(oauth2ClientManager) .innerJoin(user, eq(user.id, oauth2ClientManager.userId)) @@ -386,7 +386,7 @@ export class OAuth2Clients { await Users.grantPrivilege(subject, 'self:oauth2'); } - await db.insert(oauth2ClientManager).values({ + await DB.drizzle.insert(oauth2ClientManager).values({ clientId: client.id, userId: subject.id, issuerId: actor.id @@ -394,7 +394,7 @@ export class OAuth2Clients { } static async removeManager(client: OAuth2Client, managerId: number) { - await db + await DB.drizzle .delete(oauth2ClientManager) .where( and(eq(oauth2ClientManager.clientId, client.id), eq(oauth2ClientManager.id, managerId)) diff --git a/src/lib/server/oauth2/model/tokens.ts b/src/lib/server/oauth2/model/tokens.ts index c5b36fd..392f94c 100644 --- a/src/lib/server/oauth2/model/tokens.ts +++ b/src/lib/server/oauth2/model/tokens.ts @@ -1,5 +1,5 @@ import { - db, + DB, oauth2Client, oauth2Token, type OAuth2Client, @@ -49,7 +49,7 @@ export class OAuth2Tokens { nonce?: string, pcke?: string ) { - const [retval] = await db.insert(oauth2Token).values({ + const [retval] = await DB.drizzle.insert(oauth2Token).values({ token, type, scope, @@ -60,7 +60,7 @@ export class OAuth2Tokens { pcke }); - const [newToken] = await db + const [newToken] = await DB.drizzle .select() .from(oauth2Token) .where(eq(oauth2Token.id, retval.insertId)); @@ -69,7 +69,7 @@ export class OAuth2Tokens { } static async fetchByToken(token: string, type: OAuth2TokenType) { - const [retval] = await db + const [retval] = await DB.drizzle .select() .from(oauth2Token) .where(and(eq(oauth2Token.token, token), eq(oauth2Token.type, type))); @@ -77,7 +77,7 @@ export class OAuth2Tokens { } static async fetchByUserIdClientId(userId: number, clientId: string, type: OAuth2TokenType) { - const [retval] = await db + const [retval] = await DB.drizzle .select() .from(oauth2Token) .innerJoin(oauth2Client, eq(oauth2Token.clientId, oauth2Client.id)) @@ -92,7 +92,7 @@ export class OAuth2Tokens { } static async wipeClientTokens(client: OAuth2Client, user?: User) { - await db + await DB.drizzle .delete(oauth2Token) .where( and(eq(oauth2Token.clientId, client.id), user ? eq(oauth2Token.userId, user.id) : undefined) @@ -100,15 +100,17 @@ export class OAuth2Tokens { } static async wipeUserTokens(user: User) { - await db.delete(oauth2Token).where(eq(oauth2Token.userId, user.id)); + await DB.drizzle.delete(oauth2Token).where(eq(oauth2Token.userId, user.id)); } static async wipeExpiredTokens() { - await db.execute(sql`DELETE FROM ${oauth2Token} WHERE ${oauth2Token.expires_at} < NOW()`); + await DB.drizzle.execute( + sql`DELETE FROM ${oauth2Token} WHERE ${oauth2Token.expires_at} < NOW()` + ); } static async remove(token: OAuth2Token) { - await db.delete(oauth2Token).where(eq(oauth2Token.id, token.id)); + await DB.drizzle.delete(oauth2Token).where(eq(oauth2Token.id, token.id)); } } diff --git a/src/lib/server/oauth2/model/user.ts b/src/lib/server/oauth2/model/user.ts index d90448d..2aa7cc9 100644 --- a/src/lib/server/oauth2/model/user.ts +++ b/src/lib/server/oauth2/model/user.ts @@ -1,5 +1,5 @@ import { - db, + DB, oauth2Client, oauth2ClientAuthorization, oauth2ClientUrl, @@ -21,7 +21,7 @@ export class OAuth2Users { static async consented(userId: number, clientId: string, scopes: string | string[]) { const normalized = OAuth2Clients.splitScope(scopes); return !!( - await db + await DB.drizzle .select({ id: oauth2ClientAuthorization.id, scope: oauth2ClientAuthorization.scope @@ -43,7 +43,7 @@ export class OAuth2Users { static async saveConsent(subject: User, client: OAuth2Client, scopes: string | string[]) { const normalized = OAuth2Clients.splitScope(scopes); - const [existing] = await db + const [existing] = await DB.drizzle .select() .from(oauth2ClientAuthorization) .where( @@ -62,14 +62,14 @@ export class OAuth2Users { } }); - await db + await DB.drizzle .update(oauth2ClientAuthorization) .set({ scope: OAuth2Clients.joinScope(splitScope), current: 1, expires_at: null }) .where(eq(oauth2ClientAuthorization.id, existing.id)); return; } - await db.insert(oauth2ClientAuthorization).values({ + await DB.drizzle.insert(oauth2ClientAuthorization).values({ userId: subject.id, clientId: client.id, scope: OAuth2Clients.joinScope(normalized) @@ -81,7 +81,7 @@ export class OAuth2Users { if (!client) return false; await OAuth2Tokens.wipeClientTokens(client, subject); - await db + await DB.drizzle .update(oauth2ClientAuthorization) .set({ current: 0, expires_at: new Date() }) .where( @@ -96,7 +96,7 @@ export class OAuth2Users { } static async listAuthorizations(subject: User) { - return db + return DB.drizzle .select() .from(oauth2Client) .innerJoin(oauth2ClientAuthorization, eq(oauth2ClientAuthorization.clientId, oauth2Client.id)) diff --git a/src/lib/server/upload.ts b/src/lib/server/upload.ts index c425bb9..c522610 100644 --- a/src/lib/server/upload.ts +++ b/src/lib/server/upload.ts @@ -1,6 +1,6 @@ import { eq } from 'drizzle-orm'; import { - db, + DB, oauth2Client, upload, user, @@ -9,19 +9,41 @@ import { type User } from './drizzle'; import { Users } from './users'; -import { readFile, unlink, writeFile } from 'fs/promises'; +import { readFile, stat, unlink, writeFile } from 'fs/promises'; import { join } from 'path'; import * as mime from 'mime-types'; import { OAuth2Clients } from './oauth2'; -const userFallbackImage = await readFile(join('static', 'avatar.png')); -const clientFallbackImage = await readFile(join('static', 'application.png')); - export class Uploads { - static userFallbackImage = userFallbackImage; - static clientFallbackImage = clientFallbackImage; + static userFallbackImage: Buffer; + static clientFallbackImage: Buffer; static uploads = join('uploads'); + static async determineStaticPath() { + try { + await stat('static'); + return 'static'; + } catch { + return join('build', 'client'); + } + } + + static async getUserFallback() { + if (!Uploads.userFallbackImage) { + const staticPath = await Uploads.determineStaticPath(); + Uploads.userFallbackImage = await readFile(join(staticPath, 'avatar.png')); + } + return Uploads.userFallbackImage; + } + + static async getClientFallback() { + if (!Uploads.clientFallbackImage) { + const staticPath = await Uploads.determineStaticPath(); + Uploads.clientFallbackImage = await readFile(join(staticPath, 'application.png')); + } + return Uploads.clientFallbackImage; + } + static async removeUpload(subject: Upload) { try { unlink(join(Uploads.uploads, subject.file)); @@ -29,7 +51,7 @@ export class Uploads { // ignore unlink error } - await db.delete(upload).where(eq(upload.id, subject.id)); + await DB.drizzle.delete(upload).where(eq(upload.id, subject.id)); } static async getAvatarByUuid( @@ -40,7 +62,7 @@ export class Uploads { return undefined; } - const [picture] = await db + const [picture] = await DB.drizzle .select({ mimetype: upload.mimetype, file: upload.file }) .from(upload) .where(eq(upload.id, user.pictureId)); @@ -55,7 +77,7 @@ export class Uploads { return undefined; } - const [picture] = await db + const [picture] = await DB.drizzle .select({ mimetype: upload.mimetype, file: upload.file }) .from(upload) .where(eq(upload.id, client.pictureId)); @@ -65,23 +87,32 @@ export class Uploads { static async removeAvatar(subject: User) { if (!subject.pictureId) return; - const [fileinfo] = await db.select().from(upload).where(eq(upload.id, subject.pictureId)); + const [fileinfo] = await DB.drizzle + .select() + .from(upload) + .where(eq(upload.id, subject.pictureId)); if (fileinfo) { await Uploads.removeUpload(fileinfo); } - await db.update(user).set({ pictureId: null }).where(eq(user.id, subject.id)); + await DB.drizzle.update(user).set({ pictureId: null }).where(eq(user.id, subject.id)); } static async removeClientAvatar(client: OAuth2Client) { if (!client.pictureId) return; - const [fileinfo] = await db.select().from(upload).where(eq(upload.id, client.pictureId)); + const [fileinfo] = await DB.drizzle + .select() + .from(upload) + .where(eq(upload.id, client.pictureId)); if (fileinfo) { await Uploads.removeUpload(fileinfo); } - await db.update(oauth2Client).set({ pictureId: null }).where(eq(oauth2Client.id, client.id)); + await DB.drizzle + .update(oauth2Client) + .set({ pictureId: null }) + .where(eq(oauth2Client.id, client.id)); } static async saveAvatar(subject: User, file: File) { @@ -93,13 +124,16 @@ export class Uploads { // Remove old await Uploads.removeAvatar(subject); // Update DB - const [retval] = await db.insert(upload).values({ + const [retval] = await DB.drizzle.insert(upload).values({ original_name: file.name, mimetype: file.type, file: newName, uploaderId: subject.id }); - await db.update(user).set({ pictureId: retval.insertId }).where(eq(user.id, subject.id)); + await DB.drizzle + .update(user) + .set({ pictureId: retval.insertId }) + .where(eq(user.id, subject.id)); } static async saveClientAvatar(client: OAuth2Client, uploader: User, file: File) { @@ -111,13 +145,13 @@ export class Uploads { // Remove old await Uploads.removeClientAvatar(client); // Update DB - const [retval] = await db.insert(upload).values({ + const [retval] = await DB.drizzle.insert(upload).values({ original_name: file.name, mimetype: file.type, file: newName, uploaderId: uploader.id }); - await db + await DB.drizzle .update(oauth2Client) .set({ pictureId: retval.insertId }) .where(eq(oauth2Client.id, client.id)); diff --git a/src/lib/server/users/admin.ts b/src/lib/server/users/admin.ts index 7129aa8..3e3a9d6 100644 --- a/src/lib/server/users/admin.ts +++ b/src/lib/server/users/admin.ts @@ -1,6 +1,6 @@ import { asc, count, eq, like, or, sql } from 'drizzle-orm'; import { - db, + DB, privilege, user, userPrivilegesPrivilege, @@ -70,12 +70,12 @@ export class UsersAdmin { ) : undefined; - const [{ rowCount }] = await db + const [{ rowCount }] = await DB.drizzle .select({ rowCount: count(user.id).mapWith(Number) }) .from(user) .where(searchExpression); - const baseQuery = db + const baseQuery = DB.drizzle .select({ id: user.id }) .from(user) .where(searchExpression) @@ -84,7 +84,7 @@ export class UsersAdmin { .offset(offset) .as('searchBase'); - const junkList = await db + const junkList = await DB.drizzle .select({ user: user, user_privileges_privilege: userPrivilegesPrivilege, @@ -116,7 +116,7 @@ export class UsersAdmin { * @returns User infor */ static async getUserDetails(uuid: string) { - const junkList = await db + const junkList = await DB.drizzle .select() .from(user) .leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id)) diff --git a/src/lib/server/users/index.ts b/src/lib/server/users/index.ts index 93f35fa..71ac720 100644 --- a/src/lib/server/users/index.ts +++ b/src/lib/server/users/index.ts @@ -1,6 +1,6 @@ import bcrypt from 'bcryptjs'; import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'; -import { db, privilege, user, userPrivilegesPrivilege, type User } from '../drizzle'; +import { DB, privilege, user, userPrivilegesPrivilege, type User } from '../drizzle'; import type { UserSession } from './types'; import { error, redirect } from '@sveltejs/kit'; import { CryptoUtils } from '../crypto-utils'; @@ -16,7 +16,7 @@ export class Users { * @returns User */ static async getById(id: number): Promise { - const [result] = await db + const [result] = await DB.drizzle .select() .from(user) .where(and(eq(user.id, id), eq(user.activated, 1))) @@ -30,7 +30,7 @@ export class Users { * @returns User */ static async getByUuid(uuid: string, activatedCheck = true): Promise { - const [result] = await db + const [result] = await DB.drizzle .select() .from(user) .where(and(eq(user.uuid, uuid), activatedCheck ? eq(user.activated, 1) : undefined)) @@ -44,7 +44,7 @@ export class Users { * @returns User */ static async getByLogin(login: string): Promise { - const [result] = await db + const [result] = await DB.drizzle .select() .from(user) .where( @@ -64,7 +64,7 @@ export class Users { */ static async getBySession(session?: UserSession): Promise { if (!session) return undefined; - const [result] = await db + const [result] = await DB.drizzle .select() .from(user) .where(and(eq(user.id, session.uid), eq(user.activated, 1))) @@ -78,7 +78,7 @@ export class Users { * @param fields Fields to set */ static async update(subject: User, fields: Partial) { - return db.update(user).set(fields).where(eq(user.id, subject.id)); + return DB.drizzle.update(user).set(fields).where(eq(user.id, subject.id)); } /** @@ -137,7 +137,7 @@ export class Users { */ static async checkRegistration(username: string, email: string) { return !( - await db + await DB.drizzle .select({ id: user.id }) .from(user) .where( @@ -158,7 +158,7 @@ export class Users { const returnedToken = await UserTokens.getByToken(token, 'activation'); if (!returnedToken?.userId) return undefined; - const [userInfo] = await db + const [userInfo] = await DB.drizzle .select() .from(user) .where(eq(user.id, returnedToken.userId as number)); @@ -175,7 +175,7 @@ export class Users { * @param subject User */ static async activateUserBy(token: string, subject: User) { - await db + await DB.drizzle .update(user) .set({ activated: 1, activity_at: new Date() }) .where(eq(user.id, subject.id)); @@ -201,7 +201,7 @@ export class Users { activate?: boolean; }) { const passwordHash = await Users.hashPassword(password); - const [retval] = await db.insert(user).values({ + const [retval] = await DB.drizzle.insert(user).values({ uuid: CryptoUtils.createUUID(), email, username, @@ -211,7 +211,7 @@ export class Users { activity_at: new Date() }); - const [newUser] = await db.select().from(user).where(eq(user.id, retval.insertId)); + const [newUser] = await DB.drizzle.select().from(user).where(eq(user.id, retval.insertId)); if (EMAIL_ENABLED !== 'false' && !activate) { await Users.sendRegistrationEmail(newUser); @@ -306,7 +306,7 @@ export class Users { * @returns Available privileges */ static async getAvailablePrivileges(clientId?: number) { - return await db + return await DB.drizzle .select() .from(privilege) .where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId)); @@ -319,7 +319,7 @@ export class Users { * @returns User privileges (string list) */ static async getUserPrivileges(subject: User, clientId?: number) { - const list = await db + const list = await DB.drizzle .select({ privilege: privilege.name }) @@ -348,13 +348,13 @@ export class Users { * @returns Boolean, whether the privilege was granted or not. */ static async grantPrivilege(subject: User, name: string) { - const [existingPrivilege] = await db + const [existingPrivilege] = await DB.drizzle .select({ id: privilege.id }) .from(privilege) .where(and(eq(privilege.name, name), isNull(privilege.clientId))); if (!existingPrivilege) return false; - const [alreadyHas] = await db + const [alreadyHas] = await DB.drizzle .select({ privilegeId: userPrivilegesPrivilege.privilegeId }) .from(userPrivilegesPrivilege) .where( @@ -365,7 +365,7 @@ export class Users { ); if (alreadyHas) return true; - await db.insert(userPrivilegesPrivilege).values({ + await DB.drizzle.insert(userPrivilegesPrivilege).values({ privilegeId: existingPrivilege.id, userId: subject.id }); @@ -385,7 +385,7 @@ export class Users { // The privileges in question must actually be related to the specified client. if (clientId) { for (const id of privilegeIds) { - const [exists] = await db + const [exists] = await DB.drizzle .select({ id: privilege.id }) .from(privilege) .where(and(eq(privilege.id, id), eq(privilege.clientId, clientId))); @@ -396,7 +396,7 @@ export class Users { } } - const current = await db + const current = await DB.drizzle .select({ privilegeId: userPrivilegesPrivilege.privilegeId }) @@ -416,7 +416,7 @@ export class Users { ); if (toRemoveIds.length) { - await db + await DB.drizzle .delete(userPrivilegesPrivilege) .where( and( @@ -432,7 +432,7 @@ export class Users { ); if (toInsertIds.length) { - await db + await DB.drizzle .insert(userPrivilegesPrivilege) .values(toInsertIds.map((privilegeId) => ({ userId: subject.id, privilegeId }))); } diff --git a/src/lib/server/users/tokens.ts b/src/lib/server/users/tokens.ts index 8376612..7a63ef2 100644 --- a/src/lib/server/users/tokens.ts +++ b/src/lib/server/users/tokens.ts @@ -1,6 +1,6 @@ import { and, eq, gt, isNull, or, sql } from 'drizzle-orm'; import { CryptoUtils } from '../crypto-utils'; -import { db, userToken, type User, type UserToken } from '../drizzle'; +import { DB, userToken, type User, type UserToken } from '../drizzle'; export class UserTokens { static async create( @@ -19,17 +19,17 @@ export class UserTokens { nonce, metadata }; - const [retval] = await db.insert(userToken).values(obj); + const [retval] = await DB.drizzle.insert(userToken).values(obj); return { id: retval.insertId, ...obj } as UserToken; } static async remove(token: string | { token: string }) { const removeBy = typeof token === 'string' ? token : token.token; - await db.delete(userToken).where(eq(userToken.token, removeBy)); + await DB.drizzle.delete(userToken).where(eq(userToken.token, removeBy)); } static async getByToken(token: string, type: (typeof userToken.$inferSelect)['type']) { - const [returned] = await db + const [returned] = await DB.drizzle .select() .from(userToken) .where( @@ -44,10 +44,10 @@ export class UserTokens { } static async wipeUserTokens(user: User) { - await db.delete(userToken).where(eq(userToken.userId, user.id)); + await DB.drizzle.delete(userToken).where(eq(userToken.userId, user.id)); } static async wipeExpiredTokens() { - await db.execute(sql`DELETE FROM ${userToken} WHERE ${userToken.expires_at} < NOW()`); + await DB.drizzle.execute(sql`DELETE FROM ${userToken} WHERE ${userToken.expires_at} < NOW()`); } } diff --git a/src/lib/server/users/totp.ts b/src/lib/server/users/totp.ts index ec84d61..bac21f3 100644 --- a/src/lib/server/users/totp.ts +++ b/src/lib/server/users/totp.ts @@ -1,5 +1,5 @@ import { authenticator as totp } from 'otplib'; -import { db, userToken, type User } from '../drizzle'; +import { DB, userToken, type User } from '../drizzle'; import { and, eq, gt, isNull, or } from 'drizzle-orm'; import { PUBLIC_SITE_NAME } from '$env/static/public'; @@ -21,7 +21,7 @@ export class TimeOTP { } public static async isUserOtp(subject: PartialK) { - const tokens = await db + const tokens = await DB.drizzle .select({ id: userToken.id }) .from(userToken) .where( @@ -35,7 +35,7 @@ export class TimeOTP { } public static async getUserOtp(subject: User) { - const [token] = await db + const [token] = await DB.drizzle .select({ id: userToken.id, token: userToken.token }) .from(userToken) .where( @@ -50,7 +50,7 @@ export class TimeOTP { } public static async saveUserOtp(subject: User, secret: string) { - await db.insert(userToken).values({ + await DB.drizzle.insert(userToken).values({ type: 'totp', token: secret, userId: subject.id diff --git a/src/routes/[...wellKnown=wellKnown]/jwks.json/+server.ts b/src/routes/[...wellKnown=wellKnown]/jwks.json/+server.ts index ba6cdfb..34ddd1f 100644 --- a/src/routes/[...wellKnown=wellKnown]/jwks.json/+server.ts +++ b/src/routes/[...wellKnown=wellKnown]/jwks.json/+server.ts @@ -1,13 +1,8 @@ import { JWT_ALGORITHM } from '$env/static/private'; import { ApiUtils } from '$lib/server/api-utils'; import { JWT } from '$lib/server/jwt'; -import { exportJWK } from 'jose'; -import { v4 as uuidv4 } from 'uuid'; - -const jwks = await exportJWK(JWT.publicKey); -const kid = uuidv4({ random: Buffer.from(jwks.n as string).subarray(0, 16) }); export const GET = async () => ApiUtils.json({ - keys: [{ alg: JWT_ALGORITHM, kid, ...jwks, use: 'sig' }] + keys: [{ alg: JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }] }); diff --git a/src/routes/api/avatar/[uuid]/+server.ts b/src/routes/api/avatar/[uuid]/+server.ts index 175c621..557d994 100644 --- a/src/routes/api/avatar/[uuid]/+server.ts +++ b/src/routes/api/avatar/[uuid]/+server.ts @@ -5,7 +5,8 @@ import { join } from 'path'; export async function GET({ params: { uuid } }) { const uploadFile = await Uploads.getAvatarByUuid(uuid); if (!uploadFile) { - return new Response(Uploads.userFallbackImage, { + const fallback = await Uploads.getUserFallback(); + return new Response(fallback, { status: 200, headers: { 'Content-Type': 'image/png' diff --git a/src/routes/api/avatar/client/[uuid]/+server.ts b/src/routes/api/avatar/client/[uuid]/+server.ts index f02c2bc..40453f7 100644 --- a/src/routes/api/avatar/client/[uuid]/+server.ts +++ b/src/routes/api/avatar/client/[uuid]/+server.ts @@ -5,7 +5,8 @@ import { join } from 'path'; export async function GET({ params: { uuid } }) { const uploadFile = await Uploads.getClientAvatarById(uuid); if (!uploadFile) { - return new Response(Uploads.clientFallbackImage, { + const fallback = await Uploads.getClientFallback(); + return new Response(fallback, { status: 200, headers: { 'Content-Type': 'image/png'