This commit is contained in:
Evert Prants 2024-06-04 21:28:38 +03:00
parent 9753804b28
commit 874b4804b9
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
17 changed files with 211 additions and 118 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
uploads
node_modules
private
devdocker

29
Dockerfile Normal file
View File

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

View File

@ -1,11 +1,15 @@
import { AUTO_MIGRATE, SESSION_SECRET } from '$env/static/private'; 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 { runSeeds } from '$lib/server/drizzle/seeds';
import { JWT } from '$lib/server/jwt';
import { migrate } from 'drizzle-orm/mysql2/migrator'; import { migrate } from 'drizzle-orm/mysql2/migrator';
import { handleSession } from 'svelte-kit-cookie-session'; import { handleSession } from 'svelte-kit-cookie-session';
await DB.init();
await JWT.init();
if (AUTO_MIGRATE === 'true') { if (AUTO_MIGRATE === 'true') {
await migrate(db, { migrationsFolder: './migrations' }); await migrate(DB.drizzle, { migrationsFolder: './migrations' });
} }
await runSeeds(); await runSeeds();

View File

@ -3,12 +3,20 @@ import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import * as schema from './schema'; import * as schema from './schema';
const connection = await mysql.createConnection({ export class DB {
static mysqlConnection: mysql.Connection;
static drizzle: ReturnType<typeof drizzle<typeof schema>>;
static async init() {
DB.mysqlConnection = await mysql.createConnection({
host: DATABASE_HOST, host: DATABASE_HOST,
user: DATABASE_PASS, user: DATABASE_PASS,
password: DATABASE_PASS, password: DATABASE_PASS,
database: DATABASE_DB database: DATABASE_DB
}); });
export const db = drizzle(connection, { schema, mode: 'default' }); DB.drizzle = drizzle(DB.mysqlConnection, { schema, mode: 'default' });
}
}
export * from './schema'; export * from './schema';

View File

@ -1,5 +1,5 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db, privilege } from '..'; import { DB, privilege } from '..';
/** /**
* System privileges which must always exist in the database. * System privileges which must always exist in the database.
@ -18,11 +18,11 @@ const privileges = [
export default async function privilegesSeed() { export default async function privilegesSeed() {
for (const priv of privileges) { for (const priv of privileges) {
const [exists] = await db const [exists] = await DB.drizzle
.select({ id: privilege.id }) .select({ id: privilege.id })
.from(privilege) .from(privilege)
.where(eq(privilege.name, priv)); .where(eq(privilege.name, priv));
if (exists) continue; if (exists) continue;
await db.insert(privilege).values({ name: priv }); await DB.drizzle.insert(privilege).values({ name: priv });
} }
} }

View File

@ -1,11 +1,15 @@
import { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } from '$env/static/private'; import { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } from '$env/static/private';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { SignJWT, importPKCS8, importSPKI, jwtVerify } from 'jose'; import {
SignJWT,
const privateKeyFile = await readFile('private/jwt.private.pem', { encoding: 'utf-8' }); exportJWK,
const publicKeyFile = await readFile('private/jwt.public.pem', { encoding: 'utf-8' }); importPKCS8,
const privateKey = await importPKCS8(privateKeyFile, JWT_ALGORITHM); importSPKI,
const publicKey = await importSPKI(publicKeyFile, JWT_ALGORITHM); jwtVerify,
type JWK,
type KeyLike
} from 'jose';
import { v4 as uuidv4 } from 'uuid';
/** /**
* Generate JWTs using the following commands: * 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 * Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem
*/ */
export class JWT { export class JWT {
static privateKey = privateKey; static privateKey: KeyLike;
static publicKey = publicKey; 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<string, unknown>, subject: string, audience?: string) { static async issue(claims: Record<string, unknown>, subject: string, audience?: string) {
const sign = new SignJWT(claims) const sign = new SignJWT(claims)

View File

@ -1,7 +1,7 @@
import { PUBLIC_URL, PUBLIC_SITE_NAME } from '$env/static/public'; import { PUBLIC_URL, PUBLIC_SITE_NAME } from '$env/static/public';
import { CryptoUtils } from '$lib/server/crypto-utils'; import { CryptoUtils } from '$lib/server/crypto-utils';
import { import {
db, DB,
oauth2Client, oauth2Client,
oauth2ClientAuthorization, oauth2ClientAuthorization,
oauth2ClientManager, oauth2ClientManager,
@ -82,7 +82,7 @@ export class OAuth2Clients {
public static availableUrlTypes: OAuth2ClientURLType[] = Object.values(OAuth2ClientURLType); public static availableUrlTypes: OAuth2ClientURLType[] = Object.values(OAuth2ClientURLType);
static async fetchById(id: string | number) { static async fetchById(id: string | number) {
const [client] = await db const [client] = await DB.drizzle
.select() .select()
.from(oauth2Client) .from(oauth2Client)
.where(typeof id === 'string' ? eq(oauth2Client.client_id, id) : eq(oauth2Client.id, id)) .where(typeof id === 'string' ? eq(oauth2Client.client_id, id) : eq(oauth2Client.id, id))
@ -94,7 +94,7 @@ export class OAuth2Clients {
id: string, id: string,
type: OAuth2ClientURLType = OAuth2ClientURLType.REDIRECT_URI type: OAuth2ClientURLType = OAuth2ClientURLType.REDIRECT_URI
) { ) {
return await db return await DB.drizzle
.select() .select()
.from(oauth2ClientUrl) .from(oauth2ClientUrl)
.innerJoin(oauth2Client, eq(oauth2ClientUrl.clientId, oauth2Client.id)) .innerJoin(oauth2Client, eq(oauth2ClientUrl.clientId, oauth2Client.id))
@ -102,7 +102,7 @@ export class OAuth2Clients {
} }
static async getClientUrls(client: OAuth2Client) { static async getClientUrls(client: OAuth2Client) {
return await db return await DB.drizzle
.select() .select()
.from(oauth2ClientUrl) .from(oauth2ClientUrl)
.where(and(eq(oauth2ClientUrl.clientId, client.id))); .where(and(eq(oauth2ClientUrl.clientId, client.id)));
@ -110,7 +110,7 @@ export class OAuth2Clients {
static async checkRedirectUri(client: OAuth2Client, url: string) { static async checkRedirectUri(client: OAuth2Client, url: string) {
return !!( return !!(
await db await DB.drizzle
.select() .select()
.from(oauth2ClientUrl) .from(oauth2ClientUrl)
.innerJoin(oauth2Client, eq(oauth2ClientUrl.clientId, oauth2Client.id)) .innerJoin(oauth2Client, eq(oauth2ClientUrl.clientId, oauth2Client.id))
@ -125,7 +125,7 @@ export class OAuth2Clients {
} }
static async getAuthorizedUsers(client: OAuth2Client, userUuid?: string) { static async getAuthorizedUsers(client: OAuth2Client, userUuid?: string) {
const junkList = await db const junkList = await DB.drizzle
.select() .select()
.from(oauth2ClientAuthorization) .from(oauth2ClientAuthorization)
.innerJoin(user, eq(user.id, oauth2ClientAuthorization.userId)) .innerJoin(user, eq(user.id, oauth2ClientAuthorization.userId))
@ -215,7 +215,7 @@ export class OAuth2Clients {
) { ) {
const filterText = `%${filters?.filter?.toLowerCase()}%`; const filterText = `%${filters?.filter?.toLowerCase()}%`;
const limit = filters?.limit || 20; const limit = filters?.limit || 20;
const allowedClients = db const allowedClients = DB.drizzle
.select({ id: oauth2Client.id }) .select({ id: oauth2Client.id })
.from(oauth2Client) .from(oauth2Client)
.leftJoin(oauth2ClientManager, eq(oauth2ClientManager.clientId, oauth2Client.id)) .leftJoin(oauth2ClientManager, eq(oauth2ClientManager.clientId, oauth2Client.id))
@ -238,14 +238,14 @@ export class OAuth2Clients {
.offset(filters?.offset || 0) .offset(filters?.offset || 0)
.as('allowedClients'); .as('allowedClients');
const [{ rowCount }] = await db const [{ rowCount }] = await DB.drizzle
.select({ .select({
rowCount: count(oauth2Client.id).mapWith(Number) rowCount: count(oauth2Client.id).mapWith(Number)
}) })
.from(allowedClients) .from(allowedClients)
.innerJoin(oauth2Client, eq(allowedClients.id, oauth2Client.id)); .innerJoin(oauth2Client, eq(allowedClients.id, oauth2Client.id));
const junkList = await db const junkList = await DB.drizzle
.select({ .select({
o_auth2_client: oauth2Client, o_auth2_client: oauth2Client,
o_auth2_client_url: oauth2ClientUrl, o_auth2_client_url: oauth2ClientUrl,
@ -308,7 +308,7 @@ export class OAuth2Clients {
const uid = CryptoUtils.createUUID(); const uid = CryptoUtils.createUUID();
const secret = CryptoUtils.generateSecret(); const secret = CryptoUtils.generateSecret();
const [retval] = await db.insert(oauth2Client).values({ const [retval] = await DB.drizzle.insert(oauth2Client).values({
title, title,
description, description,
client_id: uid, client_id: uid,
@ -321,7 +321,7 @@ export class OAuth2Clients {
verified: 0 verified: 0
}); });
await db.insert(oauth2ClientUrl).values({ await DB.drizzle.insert(oauth2ClientUrl).values({
type: 'redirect_uri', type: 'redirect_uri',
url: redirect, url: redirect,
clientId: retval.insertId clientId: retval.insertId
@ -334,18 +334,18 @@ export class OAuth2Clients {
if (client.pictureId) { if (client.pictureId) {
await Uploads.removeClientAvatar(client); await Uploads.removeClientAvatar(client);
} }
await db.delete(privilege).where(eq(privilege.clientId, client.id)); await DB.drizzle.delete(privilege).where(eq(privilege.clientId, client.id));
await db.delete(oauth2Client).where(eq(oauth2Client.id, client.id)); await DB.drizzle.delete(oauth2Client).where(eq(oauth2Client.id, client.id));
} }
static async deleteUrl(client: OAuth2Client, urlId: number) { static async deleteUrl(client: OAuth2Client, urlId: number) {
await db await DB.drizzle
.delete(oauth2ClientUrl) .delete(oauth2ClientUrl)
.where(and(eq(oauth2ClientUrl.clientId, client.id), eq(oauth2ClientUrl.id, urlId))); .where(and(eq(oauth2ClientUrl.clientId, client.id), eq(oauth2ClientUrl.id, urlId)));
} }
static async addUrl(client: OAuth2Client, type: OAuth2ClientURLType, url: string) { static async addUrl(client: OAuth2Client, type: OAuth2ClientURLType, url: string) {
await db.insert(oauth2ClientUrl).values({ await DB.drizzle.insert(oauth2ClientUrl).values({
type, type,
url, url,
clientId: client.id clientId: client.id
@ -353,25 +353,25 @@ export class OAuth2Clients {
} }
static async deletePrivilege(client: OAuth2Client, privilegeId: number) { static async deletePrivilege(client: OAuth2Client, privilegeId: number) {
await db await DB.drizzle
.delete(privilege) .delete(privilege)
.where(and(eq(privilege.clientId, client.id), eq(privilege.id, privilegeId))); .where(and(eq(privilege.clientId, client.id), eq(privilege.id, privilegeId)));
} }
static async addPrivilege(client: OAuth2Client, name: string) { static async addPrivilege(client: OAuth2Client, name: string) {
const realName = `${client.client_id.split('-')[0]}:${name}`; const realName = `${client.client_id.split('-')[0]}:${name}`;
await db.insert(privilege).values({ await DB.drizzle.insert(privilege).values({
name: realName, name: realName,
clientId: client.id clientId: client.id
}); });
} }
static async update(client: OAuth2Client, body: Partial<OAuth2Client>) { static async update(client: OAuth2Client, body: Partial<OAuth2Client>) {
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) { static async getManagers(client: OAuth2Client) {
return await db return await DB.drizzle
.select({ id: oauth2ClientManager.id, email: user.email }) .select({ id: oauth2ClientManager.id, email: user.email })
.from(oauth2ClientManager) .from(oauth2ClientManager)
.innerJoin(user, eq(user.id, oauth2ClientManager.userId)) .innerJoin(user, eq(user.id, oauth2ClientManager.userId))
@ -386,7 +386,7 @@ export class OAuth2Clients {
await Users.grantPrivilege(subject, 'self:oauth2'); await Users.grantPrivilege(subject, 'self:oauth2');
} }
await db.insert(oauth2ClientManager).values({ await DB.drizzle.insert(oauth2ClientManager).values({
clientId: client.id, clientId: client.id,
userId: subject.id, userId: subject.id,
issuerId: actor.id issuerId: actor.id
@ -394,7 +394,7 @@ export class OAuth2Clients {
} }
static async removeManager(client: OAuth2Client, managerId: number) { static async removeManager(client: OAuth2Client, managerId: number) {
await db await DB.drizzle
.delete(oauth2ClientManager) .delete(oauth2ClientManager)
.where( .where(
and(eq(oauth2ClientManager.clientId, client.id), eq(oauth2ClientManager.id, managerId)) and(eq(oauth2ClientManager.clientId, client.id), eq(oauth2ClientManager.id, managerId))

View File

@ -1,5 +1,5 @@
import { import {
db, DB,
oauth2Client, oauth2Client,
oauth2Token, oauth2Token,
type OAuth2Client, type OAuth2Client,
@ -49,7 +49,7 @@ export class OAuth2Tokens {
nonce?: string, nonce?: string,
pcke?: string pcke?: string
) { ) {
const [retval] = await db.insert(oauth2Token).values({ const [retval] = await DB.drizzle.insert(oauth2Token).values({
token, token,
type, type,
scope, scope,
@ -60,7 +60,7 @@ export class OAuth2Tokens {
pcke pcke
}); });
const [newToken] = await db const [newToken] = await DB.drizzle
.select() .select()
.from(oauth2Token) .from(oauth2Token)
.where(eq(oauth2Token.id, retval.insertId)); .where(eq(oauth2Token.id, retval.insertId));
@ -69,7 +69,7 @@ export class OAuth2Tokens {
} }
static async fetchByToken(token: string, type: OAuth2TokenType) { static async fetchByToken(token: string, type: OAuth2TokenType) {
const [retval] = await db const [retval] = await DB.drizzle
.select() .select()
.from(oauth2Token) .from(oauth2Token)
.where(and(eq(oauth2Token.token, token), eq(oauth2Token.type, type))); .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) { static async fetchByUserIdClientId(userId: number, clientId: string, type: OAuth2TokenType) {
const [retval] = await db const [retval] = await DB.drizzle
.select() .select()
.from(oauth2Token) .from(oauth2Token)
.innerJoin(oauth2Client, eq(oauth2Token.clientId, oauth2Client.id)) .innerJoin(oauth2Client, eq(oauth2Token.clientId, oauth2Client.id))
@ -92,7 +92,7 @@ export class OAuth2Tokens {
} }
static async wipeClientTokens(client: OAuth2Client, user?: User) { static async wipeClientTokens(client: OAuth2Client, user?: User) {
await db await DB.drizzle
.delete(oauth2Token) .delete(oauth2Token)
.where( .where(
and(eq(oauth2Token.clientId, client.id), user ? eq(oauth2Token.userId, user.id) : undefined) 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) { 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() { 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) { 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));
} }
} }

View File

@ -1,5 +1,5 @@
import { import {
db, DB,
oauth2Client, oauth2Client,
oauth2ClientAuthorization, oauth2ClientAuthorization,
oauth2ClientUrl, oauth2ClientUrl,
@ -21,7 +21,7 @@ export class OAuth2Users {
static async consented(userId: number, clientId: string, scopes: string | string[]) { static async consented(userId: number, clientId: string, scopes: string | string[]) {
const normalized = OAuth2Clients.splitScope(scopes); const normalized = OAuth2Clients.splitScope(scopes);
return !!( return !!(
await db await DB.drizzle
.select({ .select({
id: oauth2ClientAuthorization.id, id: oauth2ClientAuthorization.id,
scope: oauth2ClientAuthorization.scope scope: oauth2ClientAuthorization.scope
@ -43,7 +43,7 @@ export class OAuth2Users {
static async saveConsent(subject: User, client: OAuth2Client, scopes: string | string[]) { static async saveConsent(subject: User, client: OAuth2Client, scopes: string | string[]) {
const normalized = OAuth2Clients.splitScope(scopes); const normalized = OAuth2Clients.splitScope(scopes);
const [existing] = await db const [existing] = await DB.drizzle
.select() .select()
.from(oauth2ClientAuthorization) .from(oauth2ClientAuthorization)
.where( .where(
@ -62,14 +62,14 @@ export class OAuth2Users {
} }
}); });
await db await DB.drizzle
.update(oauth2ClientAuthorization) .update(oauth2ClientAuthorization)
.set({ scope: OAuth2Clients.joinScope(splitScope), current: 1, expires_at: null }) .set({ scope: OAuth2Clients.joinScope(splitScope), current: 1, expires_at: null })
.where(eq(oauth2ClientAuthorization.id, existing.id)); .where(eq(oauth2ClientAuthorization.id, existing.id));
return; return;
} }
await db.insert(oauth2ClientAuthorization).values({ await DB.drizzle.insert(oauth2ClientAuthorization).values({
userId: subject.id, userId: subject.id,
clientId: client.id, clientId: client.id,
scope: OAuth2Clients.joinScope(normalized) scope: OAuth2Clients.joinScope(normalized)
@ -81,7 +81,7 @@ export class OAuth2Users {
if (!client) return false; if (!client) return false;
await OAuth2Tokens.wipeClientTokens(client, subject); await OAuth2Tokens.wipeClientTokens(client, subject);
await db await DB.drizzle
.update(oauth2ClientAuthorization) .update(oauth2ClientAuthorization)
.set({ current: 0, expires_at: new Date() }) .set({ current: 0, expires_at: new Date() })
.where( .where(
@ -96,7 +96,7 @@ export class OAuth2Users {
} }
static async listAuthorizations(subject: User) { static async listAuthorizations(subject: User) {
return db return DB.drizzle
.select() .select()
.from(oauth2Client) .from(oauth2Client)
.innerJoin(oauth2ClientAuthorization, eq(oauth2ClientAuthorization.clientId, oauth2Client.id)) .innerJoin(oauth2ClientAuthorization, eq(oauth2ClientAuthorization.clientId, oauth2Client.id))

View File

@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { import {
db, DB,
oauth2Client, oauth2Client,
upload, upload,
user, user,
@ -9,19 +9,41 @@ import {
type User type User
} from './drizzle'; } from './drizzle';
import { Users } from './users'; import { Users } from './users';
import { readFile, unlink, writeFile } from 'fs/promises'; import { readFile, stat, unlink, writeFile } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import * as mime from 'mime-types'; import * as mime from 'mime-types';
import { OAuth2Clients } from './oauth2'; import { OAuth2Clients } from './oauth2';
const userFallbackImage = await readFile(join('static', 'avatar.png'));
const clientFallbackImage = await readFile(join('static', 'application.png'));
export class Uploads { export class Uploads {
static userFallbackImage = userFallbackImage; static userFallbackImage: Buffer;
static clientFallbackImage = clientFallbackImage; static clientFallbackImage: Buffer;
static uploads = join('uploads'); 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) { static async removeUpload(subject: Upload) {
try { try {
unlink(join(Uploads.uploads, subject.file)); unlink(join(Uploads.uploads, subject.file));
@ -29,7 +51,7 @@ export class Uploads {
// ignore unlink error // 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( static async getAvatarByUuid(
@ -40,7 +62,7 @@ export class Uploads {
return undefined; return undefined;
} }
const [picture] = await db const [picture] = await DB.drizzle
.select({ mimetype: upload.mimetype, file: upload.file }) .select({ mimetype: upload.mimetype, file: upload.file })
.from(upload) .from(upload)
.where(eq(upload.id, user.pictureId)); .where(eq(upload.id, user.pictureId));
@ -55,7 +77,7 @@ export class Uploads {
return undefined; return undefined;
} }
const [picture] = await db const [picture] = await DB.drizzle
.select({ mimetype: upload.mimetype, file: upload.file }) .select({ mimetype: upload.mimetype, file: upload.file })
.from(upload) .from(upload)
.where(eq(upload.id, client.pictureId)); .where(eq(upload.id, client.pictureId));
@ -65,23 +87,32 @@ export class Uploads {
static async removeAvatar(subject: User) { static async removeAvatar(subject: User) {
if (!subject.pictureId) return; 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) { if (fileinfo) {
await Uploads.removeUpload(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) { static async removeClientAvatar(client: OAuth2Client) {
if (!client.pictureId) return; 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) { if (fileinfo) {
await Uploads.removeUpload(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) { static async saveAvatar(subject: User, file: File) {
@ -93,13 +124,16 @@ export class Uploads {
// Remove old // Remove old
await Uploads.removeAvatar(subject); await Uploads.removeAvatar(subject);
// Update DB // Update DB
const [retval] = await db.insert(upload).values({ const [retval] = await DB.drizzle.insert(upload).values({
original_name: file.name, original_name: file.name,
mimetype: file.type, mimetype: file.type,
file: newName, file: newName,
uploaderId: subject.id 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) { static async saveClientAvatar(client: OAuth2Client, uploader: User, file: File) {
@ -111,13 +145,13 @@ export class Uploads {
// Remove old // Remove old
await Uploads.removeClientAvatar(client); await Uploads.removeClientAvatar(client);
// Update DB // Update DB
const [retval] = await db.insert(upload).values({ const [retval] = await DB.drizzle.insert(upload).values({
original_name: file.name, original_name: file.name,
mimetype: file.type, mimetype: file.type,
file: newName, file: newName,
uploaderId: uploader.id uploaderId: uploader.id
}); });
await db await DB.drizzle
.update(oauth2Client) .update(oauth2Client)
.set({ pictureId: retval.insertId }) .set({ pictureId: retval.insertId })
.where(eq(oauth2Client.id, client.id)); .where(eq(oauth2Client.id, client.id));

View File

@ -1,6 +1,6 @@
import { asc, count, eq, like, or, sql } from 'drizzle-orm'; import { asc, count, eq, like, or, sql } from 'drizzle-orm';
import { import {
db, DB,
privilege, privilege,
user, user,
userPrivilegesPrivilege, userPrivilegesPrivilege,
@ -70,12 +70,12 @@ export class UsersAdmin {
) )
: undefined; : undefined;
const [{ rowCount }] = await db const [{ rowCount }] = await DB.drizzle
.select({ rowCount: count(user.id).mapWith(Number) }) .select({ rowCount: count(user.id).mapWith(Number) })
.from(user) .from(user)
.where(searchExpression); .where(searchExpression);
const baseQuery = db const baseQuery = DB.drizzle
.select({ id: user.id }) .select({ id: user.id })
.from(user) .from(user)
.where(searchExpression) .where(searchExpression)
@ -84,7 +84,7 @@ export class UsersAdmin {
.offset(offset) .offset(offset)
.as('searchBase'); .as('searchBase');
const junkList = await db const junkList = await DB.drizzle
.select({ .select({
user: user, user: user,
user_privileges_privilege: userPrivilegesPrivilege, user_privileges_privilege: userPrivilegesPrivilege,
@ -116,7 +116,7 @@ export class UsersAdmin {
* @returns User infor * @returns User infor
*/ */
static async getUserDetails(uuid: string) { static async getUserDetails(uuid: string) {
const junkList = await db const junkList = await DB.drizzle
.select() .select()
.from(user) .from(user)
.leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id)) .leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id))

View File

@ -1,6 +1,6 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'; 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 type { UserSession } from './types';
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { CryptoUtils } from '../crypto-utils'; import { CryptoUtils } from '../crypto-utils';
@ -16,7 +16,7 @@ export class Users {
* @returns User * @returns User
*/ */
static async getById(id: number): Promise<User | undefined> { static async getById(id: number): Promise<User | undefined> {
const [result] = await db const [result] = await DB.drizzle
.select() .select()
.from(user) .from(user)
.where(and(eq(user.id, id), eq(user.activated, 1))) .where(and(eq(user.id, id), eq(user.activated, 1)))
@ -30,7 +30,7 @@ export class Users {
* @returns User * @returns User
*/ */
static async getByUuid(uuid: string, activatedCheck = true): Promise<User | undefined> { static async getByUuid(uuid: string, activatedCheck = true): Promise<User | undefined> {
const [result] = await db const [result] = await DB.drizzle
.select() .select()
.from(user) .from(user)
.where(and(eq(user.uuid, uuid), activatedCheck ? eq(user.activated, 1) : undefined)) .where(and(eq(user.uuid, uuid), activatedCheck ? eq(user.activated, 1) : undefined))
@ -44,7 +44,7 @@ export class Users {
* @returns User * @returns User
*/ */
static async getByLogin(login: string): Promise<User | undefined> { static async getByLogin(login: string): Promise<User | undefined> {
const [result] = await db const [result] = await DB.drizzle
.select() .select()
.from(user) .from(user)
.where( .where(
@ -64,7 +64,7 @@ export class Users {
*/ */
static async getBySession(session?: UserSession): Promise<User | undefined> { static async getBySession(session?: UserSession): Promise<User | undefined> {
if (!session) return undefined; if (!session) return undefined;
const [result] = await db const [result] = await DB.drizzle
.select() .select()
.from(user) .from(user)
.where(and(eq(user.id, session.uid), eq(user.activated, 1))) .where(and(eq(user.id, session.uid), eq(user.activated, 1)))
@ -78,7 +78,7 @@ export class Users {
* @param fields Fields to set * @param fields Fields to set
*/ */
static async update(subject: User, fields: Partial<User>) { static async update(subject: User, fields: Partial<User>) {
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) { static async checkRegistration(username: string, email: string) {
return !( return !(
await db await DB.drizzle
.select({ id: user.id }) .select({ id: user.id })
.from(user) .from(user)
.where( .where(
@ -158,7 +158,7 @@ export class Users {
const returnedToken = await UserTokens.getByToken(token, 'activation'); const returnedToken = await UserTokens.getByToken(token, 'activation');
if (!returnedToken?.userId) return undefined; if (!returnedToken?.userId) return undefined;
const [userInfo] = await db const [userInfo] = await DB.drizzle
.select() .select()
.from(user) .from(user)
.where(eq(user.id, returnedToken.userId as number)); .where(eq(user.id, returnedToken.userId as number));
@ -175,7 +175,7 @@ export class Users {
* @param subject User * @param subject User
*/ */
static async activateUserBy(token: string, subject: User) { static async activateUserBy(token: string, subject: User) {
await db await DB.drizzle
.update(user) .update(user)
.set({ activated: 1, activity_at: new Date() }) .set({ activated: 1, activity_at: new Date() })
.where(eq(user.id, subject.id)); .where(eq(user.id, subject.id));
@ -201,7 +201,7 @@ export class Users {
activate?: boolean; activate?: boolean;
}) { }) {
const passwordHash = await Users.hashPassword(password); const passwordHash = await Users.hashPassword(password);
const [retval] = await db.insert(user).values({ const [retval] = await DB.drizzle.insert(user).values({
uuid: CryptoUtils.createUUID(), uuid: CryptoUtils.createUUID(),
email, email,
username, username,
@ -211,7 +211,7 @@ export class Users {
activity_at: new Date() 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) { if (EMAIL_ENABLED !== 'false' && !activate) {
await Users.sendRegistrationEmail(newUser); await Users.sendRegistrationEmail(newUser);
@ -306,7 +306,7 @@ export class Users {
* @returns Available privileges * @returns Available privileges
*/ */
static async getAvailablePrivileges(clientId?: number) { static async getAvailablePrivileges(clientId?: number) {
return await db return await DB.drizzle
.select() .select()
.from(privilege) .from(privilege)
.where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId)); .where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId));
@ -319,7 +319,7 @@ export class Users {
* @returns User privileges (string list) * @returns User privileges (string list)
*/ */
static async getUserPrivileges(subject: User, clientId?: number) { static async getUserPrivileges(subject: User, clientId?: number) {
const list = await db const list = await DB.drizzle
.select({ .select({
privilege: privilege.name privilege: privilege.name
}) })
@ -348,13 +348,13 @@ export class Users {
* @returns Boolean, whether the privilege was granted or not. * @returns Boolean, whether the privilege was granted or not.
*/ */
static async grantPrivilege(subject: User, name: string) { static async grantPrivilege(subject: User, name: string) {
const [existingPrivilege] = await db const [existingPrivilege] = await DB.drizzle
.select({ id: privilege.id }) .select({ id: privilege.id })
.from(privilege) .from(privilege)
.where(and(eq(privilege.name, name), isNull(privilege.clientId))); .where(and(eq(privilege.name, name), isNull(privilege.clientId)));
if (!existingPrivilege) return false; if (!existingPrivilege) return false;
const [alreadyHas] = await db const [alreadyHas] = await DB.drizzle
.select({ privilegeId: userPrivilegesPrivilege.privilegeId }) .select({ privilegeId: userPrivilegesPrivilege.privilegeId })
.from(userPrivilegesPrivilege) .from(userPrivilegesPrivilege)
.where( .where(
@ -365,7 +365,7 @@ export class Users {
); );
if (alreadyHas) return true; if (alreadyHas) return true;
await db.insert(userPrivilegesPrivilege).values({ await DB.drizzle.insert(userPrivilegesPrivilege).values({
privilegeId: existingPrivilege.id, privilegeId: existingPrivilege.id,
userId: subject.id userId: subject.id
}); });
@ -385,7 +385,7 @@ export class Users {
// The privileges in question must actually be related to the specified client. // The privileges in question must actually be related to the specified client.
if (clientId) { if (clientId) {
for (const id of privilegeIds) { for (const id of privilegeIds) {
const [exists] = await db const [exists] = await DB.drizzle
.select({ id: privilege.id }) .select({ id: privilege.id })
.from(privilege) .from(privilege)
.where(and(eq(privilege.id, id), eq(privilege.clientId, clientId))); .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({ .select({
privilegeId: userPrivilegesPrivilege.privilegeId privilegeId: userPrivilegesPrivilege.privilegeId
}) })
@ -416,7 +416,7 @@ export class Users {
); );
if (toRemoveIds.length) { if (toRemoveIds.length) {
await db await DB.drizzle
.delete(userPrivilegesPrivilege) .delete(userPrivilegesPrivilege)
.where( .where(
and( and(
@ -432,7 +432,7 @@ export class Users {
); );
if (toInsertIds.length) { if (toInsertIds.length) {
await db await DB.drizzle
.insert(userPrivilegesPrivilege) .insert(userPrivilegesPrivilege)
.values(toInsertIds.map((privilegeId) => ({ userId: subject.id, privilegeId }))); .values(toInsertIds.map((privilegeId) => ({ userId: subject.id, privilegeId })));
} }

View File

@ -1,6 +1,6 @@
import { and, eq, gt, isNull, or, sql } from 'drizzle-orm'; import { and, eq, gt, isNull, or, sql } from 'drizzle-orm';
import { CryptoUtils } from '../crypto-utils'; 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 { export class UserTokens {
static async create( static async create(
@ -19,17 +19,17 @@ export class UserTokens {
nonce, nonce,
metadata 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; return { id: retval.insertId, ...obj } as UserToken;
} }
static async remove(token: string | { token: string }) { static async remove(token: string | { token: string }) {
const removeBy = typeof token === 'string' ? token : token.token; 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']) { static async getByToken(token: string, type: (typeof userToken.$inferSelect)['type']) {
const [returned] = await db const [returned] = await DB.drizzle
.select() .select()
.from(userToken) .from(userToken)
.where( .where(
@ -44,10 +44,10 @@ export class UserTokens {
} }
static async wipeUserTokens(user: User) { 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() { 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()`);
} }
} }

View File

@ -1,5 +1,5 @@
import { authenticator as totp } from 'otplib'; 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 { and, eq, gt, isNull, or } from 'drizzle-orm';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { PUBLIC_SITE_NAME } from '$env/static/public';
@ -21,7 +21,7 @@ export class TimeOTP {
} }
public static async isUserOtp(subject: PartialK<User, 'password'>) { public static async isUserOtp(subject: PartialK<User, 'password'>) {
const tokens = await db const tokens = await DB.drizzle
.select({ id: userToken.id }) .select({ id: userToken.id })
.from(userToken) .from(userToken)
.where( .where(
@ -35,7 +35,7 @@ export class TimeOTP {
} }
public static async getUserOtp(subject: User) { public static async getUserOtp(subject: User) {
const [token] = await db const [token] = await DB.drizzle
.select({ id: userToken.id, token: userToken.token }) .select({ id: userToken.id, token: userToken.token })
.from(userToken) .from(userToken)
.where( .where(
@ -50,7 +50,7 @@ export class TimeOTP {
} }
public static async saveUserOtp(subject: User, secret: string) { public static async saveUserOtp(subject: User, secret: string) {
await db.insert(userToken).values({ await DB.drizzle.insert(userToken).values({
type: 'totp', type: 'totp',
token: secret, token: secret,
userId: subject.id userId: subject.id

View File

@ -1,13 +1,8 @@
import { JWT_ALGORITHM } from '$env/static/private'; import { JWT_ALGORITHM } from '$env/static/private';
import { ApiUtils } from '$lib/server/api-utils'; import { ApiUtils } from '$lib/server/api-utils';
import { JWT } from '$lib/server/jwt'; 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 () => export const GET = async () =>
ApiUtils.json({ ApiUtils.json({
keys: [{ alg: JWT_ALGORITHM, kid, ...jwks, use: 'sig' }] keys: [{ alg: JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }]
}); });

View File

@ -5,7 +5,8 @@ import { join } from 'path';
export async function GET({ params: { uuid } }) { export async function GET({ params: { uuid } }) {
const uploadFile = await Uploads.getAvatarByUuid(uuid); const uploadFile = await Uploads.getAvatarByUuid(uuid);
if (!uploadFile) { if (!uploadFile) {
return new Response(Uploads.userFallbackImage, { const fallback = await Uploads.getUserFallback();
return new Response(fallback, {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'image/png' 'Content-Type': 'image/png'

View File

@ -5,7 +5,8 @@ import { join } from 'path';
export async function GET({ params: { uuid } }) { export async function GET({ params: { uuid } }) {
const uploadFile = await Uploads.getClientAvatarById(uuid); const uploadFile = await Uploads.getClientAvatarById(uuid);
if (!uploadFile) { if (!uploadFile) {
return new Response(Uploads.clientFallbackImage, { const fallback = await Uploads.getClientFallback();
return new Response(fallback, {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'image/png' 'Content-Type': 'image/png'