Rotating JWT key pair
This commit is contained in:
parent
846ba5533f
commit
a86b2a346b
9
migrations/0005_happy_meltdown.sql
Normal file
9
migrations/0005_happy_meltdown.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE `jwks` (
|
||||||
|
`uuid` varchar(36) NOT NULL,
|
||||||
|
`fingerprint` varchar(64) NOT NULL,
|
||||||
|
`current` tinyint NOT NULL DEFAULT 1,
|
||||||
|
`created_at` datetime(6) NOT NULL DEFAULT current_timestamp(6),
|
||||||
|
`expires_at` datetime(6) NOT NULL,
|
||||||
|
`rotate_at` datetime(6) NOT NULL,
|
||||||
|
CONSTRAINT `jwks_uuid` PRIMARY KEY(`uuid`)
|
||||||
|
);
|
1150
migrations/meta/0005_snapshot.json
Normal file
1150
migrations/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,13 @@
|
|||||||
"when": 1717853591270,
|
"when": 1717853591270,
|
||||||
"tag": "0004_quiet_wolfsbane",
|
"tag": "0004_quiet_wolfsbane",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1733752641589,
|
||||||
|
"tag": "0005_happy_meltdown",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -38,7 +38,7 @@
|
|||||||
"reveal": "Reveal secret",
|
"reveal": "Reveal secret",
|
||||||
"regenerate": "Regenerate secret",
|
"regenerate": "Regenerate secret",
|
||||||
"activated": "Activated",
|
"activated": "Activated",
|
||||||
"verified": "Official",
|
"verified": "First-party application",
|
||||||
"scopes": "Available scopes",
|
"scopes": "Available scopes",
|
||||||
"scopesHint": "The level of access to information you will be needing for this application.",
|
"scopesHint": "The level of access to information you will be needing for this application.",
|
||||||
"grants": "Available grant types",
|
"grants": "Available grant types",
|
||||||
|
@ -9,19 +9,15 @@ export class ApiUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getJsonOrFormBody(request: Request) {
|
static async getJsonOrFormBody(request: Request) {
|
||||||
if (request.headers.get('content-type')?.startsWith('application/json')) {
|
try {
|
||||||
try {
|
if (request.headers.get('content-type')?.startsWith('application/json')) {
|
||||||
const jsonBody = await request.json();
|
const jsonBody = await request.json();
|
||||||
return jsonBody;
|
return jsonBody;
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formBody = await request.formData();
|
const formBody = await request.formData();
|
||||||
return Object.fromEntries(formBody);
|
return Object.fromEntries(formBody);
|
||||||
} catch (err) {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,11 @@ export class CryptoUtils {
|
|||||||
return v4();
|
return v4();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static fingerprintPem(pem: string) {
|
||||||
|
const der = pem.trim().split('\n').slice(1, -1).join('');
|
||||||
|
return crypto.createHash('sha256').update(der).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/q/52212430
|
// https://stackoverflow.com/q/52212430
|
||||||
/**
|
/**
|
||||||
* Symmetric encryption function
|
* Symmetric encryption function
|
||||||
|
@ -13,6 +13,20 @@ import {
|
|||||||
type AnyMySqlColumn
|
type AnyMySqlColumn
|
||||||
} from 'drizzle-orm/mysql-core';
|
} from 'drizzle-orm/mysql-core';
|
||||||
|
|
||||||
|
export const jwks = mysqlTable('jwks', {
|
||||||
|
uuid: varchar('uuid', { length: 36 }).primaryKey(),
|
||||||
|
fingerprint: varchar('fingerprint', { length: 64 }).notNull(),
|
||||||
|
current: tinyint('current').notNull().default(1),
|
||||||
|
created_at: datetime('created_at', { mode: 'date', fsp: 6 })
|
||||||
|
.default(sql`current_timestamp(6)`)
|
||||||
|
.notNull(),
|
||||||
|
expires_at: datetime('expires_at', { mode: 'date', fsp: 6 }).notNull(),
|
||||||
|
rotate_at: datetime('rotate_at', { mode: 'date', fsp: 6 }).notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type JsonKey = typeof jwks.$inferSelect;
|
||||||
|
export type NewJsonKey = typeof jwks.$inferInsert;
|
||||||
|
|
||||||
export const auditLog = mysqlTable('audit_log', {
|
export const auditLog = mysqlTable('audit_log', {
|
||||||
id: int('id').autoincrement().notNull(),
|
id: int('id').autoincrement().notNull(),
|
||||||
action: text('action').notNull(),
|
action: text('action').notNull(),
|
||||||
|
38
src/lib/server/file-backend.ts
Normal file
38
src/lib/server/file-backend.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
|
||||||
|
import { dirname, join, resolve } from 'path';
|
||||||
|
|
||||||
|
export class FileBackend {
|
||||||
|
static async fileExists(path: string | string[]) {
|
||||||
|
try {
|
||||||
|
await stat(FileBackend.filePath(path));
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveFile(path: string | string[], contents: Buffer) {
|
||||||
|
const fullPath = FileBackend.filePath(path);
|
||||||
|
try {
|
||||||
|
await mkdir(dirname(fullPath), { recursive: true });
|
||||||
|
} catch {}
|
||||||
|
return writeFile(fullPath, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async readFile<T = Buffer>(path: string | string[], encoding?: BufferEncoding) {
|
||||||
|
return readFile(FileBackend.filePath(path), { encoding }) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteFile(path: string | string[]) {
|
||||||
|
try {
|
||||||
|
await unlink(FileBackend.filePath(path));
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static filePath(path: string | string[]) {
|
||||||
|
return resolve(Array.isArray(path) ? join(...path) : path);
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,30 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { readFile } from 'fs/promises';
|
|
||||||
import {
|
import {
|
||||||
SignJWT,
|
type JWK,
|
||||||
exportJWK,
|
type KeyLike,
|
||||||
importPKCS8,
|
importPKCS8,
|
||||||
importSPKI,
|
importSPKI,
|
||||||
|
SignJWT,
|
||||||
jwtVerify,
|
jwtVerify,
|
||||||
type JWK,
|
generateKeyPair,
|
||||||
type KeyLike
|
exportPKCS8,
|
||||||
|
exportSPKI,
|
||||||
|
exportJWK
|
||||||
} from 'jose';
|
} from 'jose';
|
||||||
import { join } from 'path';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { FileBackend } from './file-backend';
|
||||||
|
import { DB, jwks, type JsonKey } from './drizzle';
|
||||||
|
import { CryptoUtils } from './crypto-utils';
|
||||||
|
|
||||||
const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env;
|
const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env;
|
||||||
|
const ISSUER_EXPIRY = 365 * 24 * 60 * 60 * 1000;
|
||||||
|
const ISSUER_ROTATE = ISSUER_EXPIRY / 2;
|
||||||
|
|
||||||
|
interface AvailableWebKey extends JsonKey {
|
||||||
|
privateKey: KeyLike;
|
||||||
|
publicKey: KeyLike;
|
||||||
|
publicKeyJWK: JWK;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate JWT keys using the following commands:
|
* Generate JWT keys using the following commands:
|
||||||
@ -20,32 +32,37 @@ const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env;
|
|||||||
* 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: KeyLike;
|
private static keys: AvailableWebKey[] = [];
|
||||||
static publicKey: KeyLike;
|
|
||||||
static jwks: JWK;
|
static getPublicJWKs() {
|
||||||
static jwksKid: string;
|
return this.keys.map((info) => ({
|
||||||
|
alg: JWT_ALGORITHM,
|
||||||
|
kid: info.uuid,
|
||||||
|
...info.publicKeyJWK,
|
||||||
|
use: 'sig'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
static async init() {
|
static async init() {
|
||||||
try {
|
const keys = await DB.drizzle.select().from(jwks);
|
||||||
const privateKeyFile = await readFile(join('private', 'jwt.private.pem'), {
|
JWT.keys = await JWT.loadKeys(keys);
|
||||||
encoding: 'utf-8'
|
|
||||||
});
|
// No current key or it is time to rotate
|
||||||
const publicKeyFile = await readFile(join('private', 'jwt.public.pem'), {
|
const keyPair = JWT.keys.find(({ current }) => current === 1);
|
||||||
encoding: 'utf-8'
|
if (!keyPair || keyPair.rotate_at.getTime() < Date.now()) {
|
||||||
});
|
const newKey = await JWT.createKeyPair();
|
||||||
JWT.privateKey = await importPKCS8(privateKeyFile, JWT_ALGORITHM);
|
if (keyPair) {
|
||||||
JWT.publicKey = await importSPKI(publicKeyFile, JWT_ALGORITHM);
|
keyPair.current = 0;
|
||||||
JWT.jwks = await exportJWK(JWT.publicKey);
|
}
|
||||||
JWT.jwksKid = uuidv4({ random: Buffer.from(JWT.jwks.n as string).subarray(0, 16) });
|
|
||||||
} catch (error) {
|
JWT.keys.push(newKey);
|
||||||
console.error('Failed to initialize the JWT backend:', error);
|
|
||||||
console.error('OpenID Connect flows will not work!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async issue(claims: Record<string, unknown>, subject: string, audience?: string) {
|
static async issue(claims: Record<string, unknown>, subject: string, audience?: string) {
|
||||||
|
const keyInfo = JWT.getCurrentKey();
|
||||||
const sign = new SignJWT(claims)
|
const sign = new SignJWT(claims)
|
||||||
.setProtectedHeader({ alg: JWT_ALGORITHM })
|
.setProtectedHeader({ alg: JWT_ALGORITHM, kid: keyInfo.uuid })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setSubject(subject)
|
.setSubject(subject)
|
||||||
.setExpirationTime(JWT_EXPIRATION)
|
.setExpirationTime(JWT_EXPIRATION)
|
||||||
@ -55,15 +72,99 @@ export class JWT {
|
|||||||
sign.setAudience(audience);
|
sign.setAudience(audience);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sign.sign(JWT.privateKey);
|
return sign.sign(keyInfo.privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async verify(token: string, subject?: string, audience?: string) {
|
static async verify(token: string, subject?: string, audience?: string) {
|
||||||
const { payload } = await jwtVerify(token, JWT.publicKey, {
|
const { payload } = await jwtVerify(
|
||||||
issuer: JWT_ISSUER,
|
token,
|
||||||
subject,
|
(header) => {
|
||||||
audience
|
const foundKey = JWT.keys.find((item) => item.uuid === header.kid);
|
||||||
});
|
if (!foundKey) {
|
||||||
|
throw new Error('Invalid kid header value');
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundKey.publicKey;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
issuer: JWT_ISSUER,
|
||||||
|
subject,
|
||||||
|
audience
|
||||||
|
}
|
||||||
|
);
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static getCurrentKey() {
|
||||||
|
const current = JWT.keys.find(({ current }) => current === 1);
|
||||||
|
if (!current) {
|
||||||
|
throw new Error('No current key found');
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async createKeyPair() {
|
||||||
|
const { privateKey, publicKey } = await generateKeyPair(JWT_ALGORITHM, {
|
||||||
|
modulusLength: 2048
|
||||||
|
});
|
||||||
|
const jwk = await exportJWK(publicKey);
|
||||||
|
const kid = uuidv4({ random: Buffer.from(jwk.n as string).subarray(0, 16) });
|
||||||
|
|
||||||
|
// Save to file backend
|
||||||
|
const exportedPrivateKey = await exportPKCS8(privateKey);
|
||||||
|
const exportedPublicKey = await exportSPKI(publicKey);
|
||||||
|
await FileBackend.saveFile(['private', kid, 'jwt.public.pem'], Buffer.from(exportedPublicKey));
|
||||||
|
await FileBackend.saveFile(
|
||||||
|
['private', kid, 'jwt.private.pem'],
|
||||||
|
Buffer.from(exportedPrivateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
const fingerprint = CryptoUtils.fingerprintPem(exportedPublicKey);
|
||||||
|
const entity = {
|
||||||
|
uuid: kid,
|
||||||
|
current: 1,
|
||||||
|
fingerprint,
|
||||||
|
created_at: new Date(),
|
||||||
|
rotate_at: new Date(Date.now() + ISSUER_ROTATE),
|
||||||
|
expires_at: new Date(Date.now() + ISSUER_EXPIRY)
|
||||||
|
} as JsonKey;
|
||||||
|
|
||||||
|
await DB.drizzle.update(jwks).set({ current: 0 });
|
||||||
|
await DB.drizzle.insert(jwks).values(entity);
|
||||||
|
|
||||||
|
// Return full pair information
|
||||||
|
return {
|
||||||
|
...entity,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
publicKeyJWK: jwk
|
||||||
|
} as AvailableWebKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadKeys(keys: JsonKey[]) {
|
||||||
|
return Promise.all(
|
||||||
|
keys.map(async (entity) => {
|
||||||
|
const publicKey = await FileBackend.readFile<string>(
|
||||||
|
['private', entity.uuid, 'jwt.public.pem'],
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const privateKey = await FileBackend.readFile<string>(
|
||||||
|
['private', entity.uuid, 'jwt.private.pem'],
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const importedPrivateKey = await importPKCS8(privateKey, JWT_ALGORITHM);
|
||||||
|
const importedPublicKey = await importSPKI(publicKey, JWT_ALGORITHM);
|
||||||
|
const jwk = await exportJWK(importedPublicKey);
|
||||||
|
|
||||||
|
// Return full pair information
|
||||||
|
return {
|
||||||
|
...entity,
|
||||||
|
privateKey: importedPrivateKey,
|
||||||
|
publicKey: importedPublicKey,
|
||||||
|
publicKeyJWK: jwk
|
||||||
|
} as AvailableWebKey;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,10 +268,10 @@ export class OAuth2AuthorizationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('Decision check passed');
|
// console.debug('Decision check passed');
|
||||||
|
|
||||||
await OAuth2Users.saveConsent(user, client, scope);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await OAuth2Users.saveConsent(user, client, scope);
|
||||||
|
|
||||||
return OAuth2AuthorizationController.posthandle(url, prehandle);
|
return OAuth2AuthorizationController.posthandle(url, prehandle);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
type User
|
type User
|
||||||
} from '$lib/server/drizzle';
|
} from '$lib/server/drizzle';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { and, eq } from 'drizzle-orm';
|
import { and, eq, gt, isNull, or } from 'drizzle-orm';
|
||||||
import { OAuth2Clients } from './client';
|
import { OAuth2Clients } from './client';
|
||||||
import { OAuth2Tokens } from './tokens';
|
import { OAuth2Tokens } from './tokens';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
@ -32,7 +32,11 @@ export class OAuth2Users {
|
|||||||
and(
|
and(
|
||||||
eq(oauth2Client.client_id, clientId),
|
eq(oauth2Client.client_id, clientId),
|
||||||
eq(oauth2ClientAuthorization.userId, userId),
|
eq(oauth2ClientAuthorization.userId, userId),
|
||||||
eq(oauth2ClientAuthorization.current, 1)
|
eq(oauth2ClientAuthorization.current, 1),
|
||||||
|
or(
|
||||||
|
isNull(oauth2ClientAuthorization.expires_at),
|
||||||
|
gt(oauth2ClientAuthorization.expires_at, new Date())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).filter(({ scope }) => {
|
).filter(({ scope }) => {
|
||||||
@ -54,6 +58,9 @@ export class OAuth2Users {
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
// Two week validity for consent
|
||||||
|
const nextExpiry = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const splitScope = OAuth2Clients.splitScope(existing.scope || '');
|
const splitScope = OAuth2Clients.splitScope(existing.scope || '');
|
||||||
normalized.forEach((entry) => {
|
normalized.forEach((entry) => {
|
||||||
@ -64,7 +71,7 @@ export class OAuth2Users {
|
|||||||
|
|
||||||
await DB.drizzle
|
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: nextExpiry })
|
||||||
.where(eq(oauth2ClientAuthorization.id, existing.id));
|
.where(eq(oauth2ClientAuthorization.id, existing.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -72,6 +79,7 @@ export class OAuth2Users {
|
|||||||
await DB.drizzle.insert(oauth2ClientAuthorization).values({
|
await DB.drizzle.insert(oauth2ClientAuthorization).values({
|
||||||
userId: subject.id,
|
userId: subject.id,
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
|
expires_at: nextExpiry,
|
||||||
scope: OAuth2Clients.joinScope(normalized)
|
scope: OAuth2Clients.joinScope(normalized)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,14 @@ import {
|
|||||||
type User
|
type User
|
||||||
} from './drizzle';
|
} from './drizzle';
|
||||||
import { Users } from './users';
|
import { Users } from './users';
|
||||||
import { readFile, stat, unlink, writeFile } from 'fs/promises';
|
import { readFile, stat } 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';
|
||||||
import { MAX_FILE_SIZE_MB, ALLOWED_IMAGES } from '$lib/constants';
|
import { MAX_FILE_SIZE_MB, ALLOWED_IMAGES } from '$lib/constants';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import imageSize from 'image-size';
|
import imageSize from 'image-size';
|
||||||
|
import { FileBackend } from './file-backend';
|
||||||
|
|
||||||
export class Uploads {
|
export class Uploads {
|
||||||
static userFallbackImage: Buffer;
|
static userFallbackImage: Buffer;
|
||||||
@ -48,12 +49,7 @@ export class Uploads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async removeUpload(subject: Upload) {
|
static async removeUpload(subject: Upload) {
|
||||||
try {
|
await FileBackend.deleteFile([Uploads.uploads, subject.file]);
|
||||||
await unlink(join(Uploads.uploads, subject.file));
|
|
||||||
} catch {
|
|
||||||
// ignore unlink error
|
|
||||||
}
|
|
||||||
|
|
||||||
await DB.drizzle.delete(upload).where(eq(upload.id, subject.id));
|
await DB.drizzle.delete(upload).where(eq(upload.id, subject.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +138,7 @@ export class Uploads {
|
|||||||
const newName = `user-${subject.uuid.split('-')[0]}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
const newName = `user-${subject.uuid.split('-')[0]}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
||||||
const buffer = await Uploads.ensureAllowedFile(file);
|
const buffer = await Uploads.ensureAllowedFile(file);
|
||||||
// Write to filesystem
|
// Write to filesystem
|
||||||
await writeFile(join(Uploads.uploads, newName), buffer);
|
await FileBackend.saveFile([Uploads.uploads, newName], buffer);
|
||||||
// Remove old
|
// Remove old
|
||||||
await Uploads.removeAvatar(subject);
|
await Uploads.removeAvatar(subject);
|
||||||
// Update DB
|
// Update DB
|
||||||
@ -163,7 +159,7 @@ export class Uploads {
|
|||||||
const newName = `client-${client.client_id.substring(0, 8)}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
const newName = `client-${client.client_id.substring(0, 8)}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
||||||
const buffer = await Uploads.ensureAllowedFile(file);
|
const buffer = await Uploads.ensureAllowedFile(file);
|
||||||
// Write to filesystem
|
// Write to filesystem
|
||||||
await writeFile(join(Uploads.uploads, newName), buffer);
|
await FileBackend.saveFile([Uploads.uploads, newName], buffer);
|
||||||
// Remove old
|
// Remove old
|
||||||
await Uploads.removeClientAvatar(client);
|
await Uploads.removeClientAvatar(client);
|
||||||
// Update DB
|
// Update DB
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
|
||||||
import { JWT } from '$lib/server/jwt';
|
import { JWT } from '$lib/server/jwt';
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const GET = async () =>
|
export const GET = async () =>
|
||||||
json({
|
json({
|
||||||
keys: [{ alg: env.JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }]
|
keys: JWT.getPublicJWKs()
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
|
import { FileBackend } from '$lib/server/file-backend.js';
|
||||||
import { Uploads } from '$lib/server/upload.js';
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
import { readFile } from 'fs/promises';
|
|
||||||
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);
|
||||||
@ -14,7 +13,7 @@ export async function GET({ params: { uuid } }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const readUpload = await readFile(join(Uploads.uploads, uploadFile.file));
|
const readUpload = await FileBackend.readFile([Uploads.uploads, uploadFile.file]);
|
||||||
return new Response(readUpload, {
|
return new Response(readUpload, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
|
import { FileBackend } from '$lib/server/file-backend.js';
|
||||||
import { Uploads } from '$lib/server/upload.js';
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
import { readFile } from 'fs/promises';
|
|
||||||
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);
|
||||||
@ -14,7 +13,7 @@ export async function GET({ params: { uuid } }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const readUpload = await readFile(join(Uploads.uploads, uploadFile.file));
|
const readUpload = await FileBackend.readFile([Uploads.uploads, uploadFile.file]);
|
||||||
return new Response(readUpload, {
|
return new Response(readUpload, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
Loading…
Reference in New Issue
Block a user