feat: move oauth api to fastify + update deps

This commit is contained in:
diced
2025-02-21 23:33:48 -08:00
parent 1d92599788
commit af68a2a06c
23 changed files with 2657 additions and 2933 deletions

View File

@@ -20,90 +20,90 @@
"db:migrate": "prisma migrate dev --create-only"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.717.0",
"@fastify/cookie": "^9.4.0",
"@fastify/cors": "^9.0.1",
"@fastify/multipart": "^8.3.0",
"@fastify/rate-limit": "^9.1.0",
"@fastify/sensible": "^5.6.0",
"@fastify/static": "^7.0.4",
"@aws-sdk/client-s3": "^3.750.0",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^10.0.2",
"@fastify/multipart": "^9.0.3",
"@fastify/rate-limit": "^10.2.2",
"@fastify/sensible": "^6.0.3",
"@fastify/static": "^8.1.1",
"@github/webauthn-json": "^2.1.1",
"@mantine/charts": "^7.15.1",
"@mantine/code-highlight": "^7.15.1",
"@mantine/core": "^7.15.1",
"@mantine/dates": "^7.15.1",
"@mantine/dropzone": "^7.15.1",
"@mantine/form": "^7.15.1",
"@mantine/hooks": "^7.15.1",
"@mantine/modals": "^7.15.1",
"@mantine/notifications": "^7.15.1",
"@prisma/client": "^6.1.0",
"@prisma/internals": "^6.1.0",
"@prisma/migrate": "^6.1.0",
"@tabler/icons-react": "^3.26.0",
"@xoi/gps-metadata-remover": "^1.1.2",
"@mantine/charts": "^7.17.0",
"@mantine/code-highlight": "^7.17.0",
"@mantine/core": "^7.17.0",
"@mantine/dates": "^7.17.0",
"@mantine/dropzone": "^7.17.0",
"@mantine/form": "^7.17.0",
"@mantine/hooks": "^7.17.0",
"@mantine/modals": "^7.17.0",
"@mantine/notifications": "^7.17.0",
"@prisma/client": "^6.4.1",
"@prisma/internals": "^6.4.1",
"@prisma/migrate": "^6.4.1",
"@tabler/icons-react": "^3.30.0",
"@xoi/gps-metadata-remover": "^2.0.0",
"argon2": "^0.41.1",
"bytes": "^3.1.2",
"clsx": "^2.1.1",
"colorette": "^2.0.20",
"commander": "^12.1.0",
"commander": "^13.1.0",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"fast-glob": "^3.3.2",
"fastify": "^4.29.0",
"fastify-plugin": "^4.5.1",
"fast-glob": "^3.3.3",
"fastify": "^5.2.1",
"fastify-plugin": "^5.0.1",
"fflate": "^0.8.2",
"fluent-ffmpeg": "^2.1.3",
"highlight.js": "^11.11.0",
"highlight.js": "^11.11.1",
"iron-session": "^8.0.4",
"isomorphic-dompurify": "^2.19.0",
"katex": "^0.16.18",
"isomorphic-dompurify": "^2.22.0",
"katex": "^0.16.21",
"mantine-datatable": "^7.15.1",
"ms": "^2.1.3",
"multer": "1.4.5-lts.1",
"next": "^15.1.2",
"next": "^15.1.7",
"otplib": "^12.0.1",
"prisma": "^6.1.0",
"prisma": "^6.4.1",
"qrcode": "^1.5.4",
"react": "^19.0.0-rc.1",
"react-dom": "^19.0.0-rc.1",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0",
"react-markdown": "^10.0.0",
"remark-gfm": "^4.0.1",
"sharp": "^0.33.5",
"swr": "^2.2.5",
"zod": "^3.24.1",
"zustand": "^5.0.2"
"swr": "^2.3.2",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/compat": "^1.2.4",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@eslint/compat": "^1.2.7",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@types/bytes": "^3.1.5",
"@types/express": "^4.17.21",
"@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.27",
"@types/katex": "^0.16.7",
"@types/ms": "^0.7.34",
"@types/ms": "^2.1.0",
"@types/multer": "^1.4.12",
"@types/node": "^20.17.10",
"@types/node": "^22.13.5",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^9.17.0",
"eslint-config-next": "^15.1.2",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"eslint": "^9.21.0",
"eslint-config-next": "^15.1.7",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-unused-imports": "^4.1.4",
"postcss": "^8.4.49",
"postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.4.2",
"prettier": "^3.5.2",
"tsc-alias": "^1.8.10",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
"tsup": "^8.3.6",
"tsx": "^4.19.3",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=22"

5070
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["omitApi"]
}
datasource db {

View File

@@ -40,7 +40,6 @@ export function settingsOnSubmit(router: NextRouter, form: ReturnType<typeof use
});
await fetch('/reload');
await fetch('/api/reload');
mutate('/api/server/settings', data);
router.replace(router.asPath, undefined, { scroll: false });
}

View File

@@ -95,7 +95,6 @@ export default function ImportButton() {
});
await fetch('/reload');
await fetch('/api/reload');
}
};

View File

@@ -30,7 +30,7 @@ import {
IconTrashFilled,
IconWriting,
} from '@tabler/icons-react';
import ms from 'ms';
import ms, { StringValue } from 'ms';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
@@ -133,8 +133,8 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
The file will automatically delete itself after this time.{' '}
{config.files.defaultExpiration ? (
<>
The default expiration time is <b>{ms(config.files.defaultExpiration)}</b> (you can
override this with the below option).
The default expiration time is <b>{ms(config.files.defaultExpiration as StringValue)}</b>{' '}
(you can override this with the below option).
</>
) : (
<>

View File

@@ -8,9 +8,9 @@ export default function Markdown({ md }: { md: string }) {
<Paper withBorder p='md'>
<ReactMarkdown
components={{
code({ node: _, inline, className, children, ...props }) {
code({ node: _, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
return match ? (
<HighlightCode language={match[1]} code={String(children).replace(/\n$/, '')} />
) : (
<Code className={className} {...props}>

View File

@@ -1,9 +1,8 @@
import { ApiAuthOauthResponse } from '@/pages/api/auth/oauth';
import { ApiAuthInvitesResponse } from '@/server/routes/api/auth/invites';
import { ApiAuthInvitesIdResponse } from '@/server/routes/api/auth/invites/[id]';
import { ApiLoginResponse } from '@/server/routes/api/auth/login';
import { ApiLogoutResponse } from '@/server/routes/api/auth/logout';
import { ApiAuthOauthResponse } from '@/server/routes/api/auth/oauth';
import { ApiAuthRegisterResponse } from '@/server/routes/api/auth/register';
import { ApiAuthWebauthnResponse } from '@/server/routes/api/auth/webauthn';
import { ApiHealthcheckResponse } from '@/server/routes/api/healthcheck';

View File

@@ -1,4 +1,4 @@
import msFn from 'ms';
import msFn, { StringValue } from 'ms';
import { log } from '../logger';
import { bytes } from '../bytes';
import { prisma } from '../db';
@@ -446,7 +446,7 @@ function parse(value: string, type: EnvType) {
case 'byte':
return bytes(value);
case 'ms':
return msFn(value);
return msFn(value as StringValue);
case 'json[]':
try {
return JSON.parse(value);

View File

@@ -147,7 +147,7 @@ export function parseHeaders(headers: UploadHeaders, fileConfig: Config['files']
response.deletesAt = expiresAt;
} else {
if (fileConfig.defaultExpiration) {
const expiresAt = new Date(Date.now() + ms(fileConfig.defaultExpiration));
const expiresAt = new Date(Date.now() + ms(fileConfig.defaultExpiration as StringValue));
response.deletesAt = expiresAt;
}
}

View File

@@ -84,7 +84,7 @@ async function generate(config: Config, datasource: Datasource, ids: string[]) {
stream.pipe(writeStream);
stream.on('error', reject);
writeStream.on('error', reject);
writeStream.on('finish', resolve);
writeStream.on('finish', resolve as any);
});
const thumbnailTmpFile = join(config.core.tempDirectory, `zthumbnail_${file.id}.jpg`);

View File

@@ -1,59 +0,0 @@
import { prisma } from '@/lib/db';
import { log } from '@/lib/logger';
import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method';
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
import { NextApiReq, NextApiRes } from '@/lib/response';
import { OAuthProvider, OAuthProviderType } from '@prisma/client';
export type ApiAuthOauthResponse = OAuthProvider[];
type Body = {
provider?: OAuthProviderType;
};
const logger = log('api').c('auth').c('oauth');
async function handler(req: NextApiReq<Body>, res: NextApiRes<ApiAuthOauthResponse>) {
if (req.method === 'DELETE') {
const { password } = (await prisma.user.findFirst({
where: {
id: req.user.id,
},
select: {
password: true,
},
}))!;
if (!req.user.oauthProviders.length) return res.badRequest('No providers to delete');
if (req.user.oauthProviders.length === 1 && !password)
return res.badRequest("You can't your last oauth provider without a password");
const { provider } = req.body;
if (!provider) return res.badRequest('Provider is required');
const providers = await prisma.user.update({
where: {
id: req.user.id,
},
data: {
oauthProviders: {
deleteMany: [{ provider }],
},
},
include: {
oauthProviders: true,
},
});
logger.info(`${req.user.username} unlinked an oauth provider`, {
provider,
});
return res.ok(providers.oauthProviders);
} else {
return res.ok(req.user.oauthProviders);
}
}
export default combine([method(['GET', 'DELETE']), ziplineAuth()], handler);

View File

@@ -1,25 +0,0 @@
import { reloadSettings } from '@/lib/config';
import { prisma } from '@/lib/db';
import { isAdministrator } from '@/lib/role';
import { getSession } from '@/server/session';
import { NextApiRequest, NextApiResponse } from 'next/types';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession(req, res);
if (!session.id || !session.sessionId) return res.redirect(302, '/auth/login');
const user = await prisma.user.findFirst({
where: {
sessions: {
has: session.sessionId,
},
},
});
if (!user) return res.redirect(302, '/dashboard');
if (!isAdministrator(user.role)) return res.redirect(302, '/dashboard');
await reloadSettings();
return res.json({ success: true });
}

View File

@@ -20,7 +20,7 @@ import { fastifySensible } from '@fastify/sensible';
import { fastifyStatic } from '@fastify/static';
import fastify from 'fastify';
import { mkdir, readFile } from 'fs/promises';
import ms from 'ms';
import ms, { StringValue } from 'ms';
import { parse } from 'url';
import { version } from '../../package.json';
import { checkRateLimit } from './plugins/checkRateLimit';
@@ -28,6 +28,7 @@ import next, { ALL_METHODS } from './plugins/next';
import loadRoutes from './routes';
import { filesRoute } from './routes/files.dy';
import { urlsRoute } from './routes/urls.dy';
import oauthPlugin from './plugins/oauth';
const MODE = process.env.NODE_ENV || 'production';
const logger = log('server');
@@ -95,6 +96,8 @@ async function main() {
root: config.core.tempDirectory,
});
await server.register(oauthPlugin);
if (config.ratelimit.enabled) {
try {
checkRateLimit(config);
@@ -169,22 +172,22 @@ async function main() {
server.next('/reload', ALL_METHODS);
}
// TODO: no longer need this when all the api routes are handled by fastify :)
const routeKeys = Object.keys(routes); // holds "currently migrated routes" so we can parse json through fastify
server.addContentTypeParser('application/json', (req, body, done) => {
if (routeKeys.includes(req.routeOptions.config.url)) {
let bodyString = '';
body.on('data', (chunk) => {
bodyString += chunk;
});
// // TODO: no longer need this when all the api routes are handled by fastify :)
// const routeKeys = Object.keys(routes); // holds "currently migrated routes" so we can parse json through fastify
// server.addContentTypeParser('application/json', (req, body, done) => {
// if (routeKeys.includes(req.routeOptions.config.url)) {
// let bodyString = '';
// body.on('data', (chunk) => {
// bodyString += chunk;
// });
body.on('end', () => {
if (bodyString === '' || bodyString === null) return done(null, {});
// body.on('end', () => {
// if (bodyString === '' || bodyString === null) return done(null, {});
server.getDefaultJsonParser('error', 'ignore')(req, bodyString, done);
});
} else done(null, body);
});
// server.getDefaultJsonParser('error', 'ignore')(req, bodyString, done);
// });
// } else done(null, body);
// });
server.setErrorHandler((error, _, res) => {
if (error.statusCode) {
@@ -209,10 +212,11 @@ async function main() {
logger.info('server started', { hostname: config.core.hostname, port: config.core.port });
// Tasks
tasks.interval('deletefiles', ms(config.tasks.deleteInterval), deleteFiles(prisma));
tasks.interval('maxviews', ms(config.tasks.maxViewsInterval), maxViews(prisma));
tasks.interval('deletefiles', ms(config.tasks.deleteInterval as StringValue), deleteFiles(prisma));
tasks.interval('maxviews', ms(config.tasks.maxViewsInterval as StringValue), maxViews(prisma));
if (config.features.metrics) tasks.interval('metrics', ms(config.tasks.metricsInterval), metrics(prisma));
if (config.features.metrics)
tasks.interval('metrics', ms(config.tasks.metricsInterval as StringValue), metrics(prisma));
if (config.features.thumbnails.enabled) {
for (let i = 0; i !== config.features.thumbnails.num_threads; ++i) {
@@ -222,8 +226,12 @@ async function main() {
});
}
tasks.interval('thumbnails', ms(config.tasks.thumbnailsInterval), thumbnails(prisma));
tasks.interval('clearinvites', ms(config.tasks.clearInvitesInterval), clearInvites(prisma));
tasks.interval('thumbnails', ms(config.tasks.thumbnailsInterval as StringValue), thumbnails(prisma));
tasks.interval(
'clearinvites',
ms(config.tasks.clearInvitesInterval as StringValue),
clearInvites(prisma),
);
}
tasks.start();

View File

@@ -3,18 +3,7 @@ import fastifyPlugin from 'fastify-plugin';
import next from 'next';
import { NextServerOptions, RequestHandler } from 'next/dist/server/next';
export const ALL_METHODS: HTTPMethods[] = [
'DELETE',
'GET',
'HEAD',
'PATCH',
'POST',
'PUT',
// 'OPTIONS',
'COPY',
'MOVE',
'TRACE',
];
export const ALL_METHODS: HTTPMethods[] = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
async function nextPlugin(fastify: FastifyInstance, options: NextServerOptions) {
const nextServer = next(options);
@@ -48,7 +37,7 @@ async function nextPlugin(fastify: FastifyInstance, options: NextServerOptions)
export default fastifyPlugin(nextPlugin, {
name: 'next',
fastify: '4.x',
fastify: '5.x',
});
declare module 'fastify' {

View File

@@ -1,12 +1,12 @@
import { NextApiReq, NextApiRes } from '@/lib/response';
import { OAuthProviderType } from '@prisma/client';
import { prisma } from '../db';
import { findProvider } from './providerUtil';
import { createToken, decrypt } from '../crypto';
import { config } from '../config';
import { User } from '../db/models/user';
import Logger, { log } from '../logger';
import { getSession, saveSession } from '@/server/session';
import { config } from '@/lib/config';
import { createToken, decrypt } from '@/lib/crypto';
import { prisma } from '@/lib/db';
import Logger, { log } from '@/lib/logger';
import { findProvider } from '@/lib/oauth/providerUtil';
import { OAuthProviderType, User } from '@prisma/client';
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { getSession, saveSession } from '../session';
export interface OAuthQuery {
state?: string;
@@ -26,31 +26,31 @@ export interface OAuthResponse {
redirect?: string;
}
export const withOAuth =
(
async function oauthPlugin(fastify: FastifyInstance) {
fastify.decorateRequest('oauthHandle', oauthHandle);
async function oauthHandle(
this: FastifyRequest,
reply: FastifyReply,
provider: OAuthProviderType,
oauthProfile: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
) =>
async (req: NextApiReq, res: NextApiRes) => {
handler: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
) {
const logger = log('api').c('auth').c('oauth').c(provider.toLowerCase());
req.query.host = req.headers.host ?? 'localhost:3000';
(this.query as any).host = this.headers.host ?? 'localhost:3000';
const response = await oauthProfile(req.query as OAuthQuery, logger);
const session = await getSession(req, res);
const response = await handler(this.query as OAuthQuery, logger);
const session = await getSession(this, reply);
if (response.error) {
logger.warn('invalid oauth request', {
error: response.error,
});
return res.serverError(response.error, {
oauth: response.error_code,
});
return reply.internalServerError(response.error_code + ' ' + response.error);
}
if (response.redirect) {
return res.redirect(response.redirect);
return reply.redirect(response.redirect);
}
logger.debug('oauth response', {
@@ -76,7 +76,7 @@ export const withOAuth =
},
});
const { state } = req.query as OAuthQuery;
const { state } = this.query as OAuthQuery;
const user = await prisma.user.findFirst({
where: {
@@ -99,10 +99,10 @@ export const withOAuth =
}
if (urlState === 'link') {
if (!user) return res.unauthorized('invalid session');
if (!user) return reply.unauthorized('invalid session');
if (findProvider(provider, user.oauthProviders))
return res.badRequest('This account is already linked to this provider');
return reply.badRequest('This account is already linked to this provider');
logger.debug('attempting to link oauth account', {
provider,
@@ -134,7 +134,7 @@ export const withOAuth =
user: user.id,
});
return res.redirect('/dashboard/settings');
return reply.redirect('/dashboard/settings');
} catch (e) {
logger.error('failed to link oauth account', {
provider,
@@ -142,7 +142,7 @@ export const withOAuth =
error: e,
});
return res.badRequest('Cant link account, already linked with this provider');
return reply.badRequest('Cant link account, already linked with this provider');
}
} else if (user && userOauth) {
await prisma.oAuthProvider.update({
@@ -164,7 +164,7 @@ export const withOAuth =
user: user.id,
});
return res.redirect('/dashboard');
return reply.redirect('/dashboard');
} else if (existingOauth) {
const login = await prisma.oAuthProvider.update({
where: {
@@ -188,15 +188,15 @@ export const withOAuth =
user: login.user!.id,
});
return res.redirect('/dashboard');
return reply.redirect('/dashboard');
} else if (config.oauth.loginOnly) {
logger.warn('user tried to create account with oauth, but login only is enabled', {
oauth: response.username || 'unknown',
ua: req.headers['user-agent'],
ua: this.headers['user-agent'],
});
return res.badRequest("Can't create users through oauth.");
return reply.badRequest("Can't create users through oauth.");
} else if (existingUser) {
return res.badRequest('This username is already taken');
return reply.badRequest('This username is already taken');
}
try {
@@ -224,20 +224,36 @@ export const withOAuth =
user: nuser.id,
});
return res.redirect('/dashboard');
return reply.redirect('/dashboard');
} catch (e) {
if ((e as { code: string }).code === 'P2002') {
// already linked can't create, last failsafe lol
logger.warn('user tried to create account with oauth, but already linked', {
oauth: response.username || 'unknown',
ua: req.headers['user-agent'],
ua: this.headers['user-agent'],
});
logger.debug('oauth create error', {
error: e,
response,
});
return res.badRequest('Cant create user, already linked with this provider');
return reply.badRequest('Cant create user, already linked with this provider');
} else throw e;
}
};
}
}
export default fastifyPlugin(oauthPlugin, {
name: 'oauth',
fastify: '5.x',
});
declare module 'fastify' {
interface FastifyRequest {
oauthHandle: (
reply: FastifyReply,
provider: OAuthProviderType,
handler: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
) => void;
}
}

View File

@@ -1,14 +1,13 @@
import { fetchToDataURL } from '@/lib/base64';
import { config } from '@/lib/config';
import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method';
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
import { encrypt } from '@/lib/crypto';
import Logger from '@/lib/logger';
import enabled from '@/lib/oauth/enabled';
import { discordAuth } from '@/lib/oauth/providerUtil';
import { fetchToDataURL } from '@/lib/base64';
import Logger from '@/lib/logger';
import { encrypt } from '@/lib/crypto';
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
import fastifyPlugin from 'fastify-plugin';
async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
async function discordOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauthRegistration)
return {
error: 'OAuth registration is disabled.',
@@ -87,4 +86,14 @@ async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promi
};
}
export default combine([method(['GET'])], withOAuth('DISCORD', handler));
export const PATH = '/api/auth/oauth/discord';
export default fastifyPlugin(
(server, _, done) => {
server.get(PATH, async (req, res) => {
return req.oauthHandle(res, 'DISCORD', discordOauth);
});
done();
},
{ name: PATH },
);

View File

@@ -2,13 +2,12 @@ import { fetchToDataURL } from '@/lib/base64';
import { config } from '@/lib/config';
import { encrypt } from '@/lib/crypto';
import Logger from '@/lib/logger';
import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method';
import enabled from '@/lib/oauth/enabled';
import { githubAuth } from '@/lib/oauth/providerUtil';
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
import fastifyPlugin from 'fastify-plugin';
async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
async function githubOauth({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauthRegistration)
return {
error: 'OAuth registration is disabled.',
@@ -88,4 +87,14 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
};
}
export default combine([method(['GET'])], withOAuth('GITHUB', handler));
export const PATH = '/api/auth/oauth/github';
export default fastifyPlugin(
(server, _, done) => {
server.get(PATH, async (req, res) => {
return req.oauthHandle(res, 'GITHUB', githubOauth);
});
done();
},
{ name: PATH },
);

View File

@@ -2,13 +2,12 @@ import { fetchToDataURL } from '@/lib/base64';
import { config } from '@/lib/config';
import { encrypt } from '@/lib/crypto';
import Logger from '@/lib/logger';
import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method';
import enabled from '@/lib/oauth/enabled';
import { googleAuth } from '@/lib/oauth/providerUtil';
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
import fastifyPlugin from 'fastify-plugin';
async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
async function googleOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauthRegistration)
return {
error: 'OAuth registration is disabled.',
@@ -79,4 +78,14 @@ async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promi
};
}
export default combine([method(['GET'])], withOAuth('GOOGLE', handler));
export const PATH = '/api/auth/oauth/google';
export default fastifyPlugin(
(server, _, done) => {
server.get(PATH, async (req, res) => {
return req.oauthHandle(res, 'GOOGLE', googleOauth);
});
done();
},
{ name: PATH },
);

View File

@@ -0,0 +1,63 @@
import { log } from '@/lib/logger';
import fastifyPlugin from 'fastify-plugin';
import { OAuthProvider, OAuthProviderType } from '@prisma/client';
import { userMiddleware } from '@/server/middleware/user';
import { prisma } from '@/lib/db';
export type ApiAuthOauthResponse = OAuthProvider[];
type Body = {
provider?: OAuthProviderType;
};
const logger = log('api').c('auth').c('oauth');
export const PATH = '/api/auth/oauth/oidc';
export default fastifyPlugin(
(server, _, done) => {
server.get(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
return res.send(req.user.oauthProviders);
});
server.delete<{ Body: Body }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
const { password } = (await prisma.user.findFirst({
where: {
id: req.user.id,
},
select: {
password: true,
},
}))!;
if (!req.user.oauthProviders.length) return res.badRequest('No providers to delete');
if (req.user.oauthProviders.length === 1 && !password)
return res.badRequest("You can't delete your last oauth provider without a password");
const { provider } = req.body;
if (!provider) return res.badRequest('Provider is required');
const providers = await prisma.user.update({
where: {
id: req.user.id,
},
data: {
oauthProviders: {
deleteMany: [{ provider }],
},
},
include: {
oauthProviders: true,
},
});
logger.info(`${req.user.username} unlinked an oauth provider`, {
provider,
});
return res.send(providers.oauthProviders);
});
done();
},
{ name: PATH },
);

View File

@@ -2,14 +2,12 @@ import { fetchToDataURL } from '@/lib/base64';
import { config } from '@/lib/config';
import { encrypt } from '@/lib/crypto';
import Logger from '@/lib/logger';
import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method';
import enabled from '@/lib/oauth/enabled';
import { oidcAuth } from '@/lib/oauth/providerUtil';
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
import fastifyPlugin from 'fastify-plugin';
// thanks to @danejur for this https://github.com/diced/zipline/pull/372
async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
async function oidcOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauthRegistration)
return {
error: 'OAuth registration is disabled.',
@@ -85,4 +83,14 @@ async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promi
};
}
export default combine([method(['GET'])], withOAuth('OIDC', handler));
export const PATH = '/api/auth/oauth/oidc';
export default fastifyPlugin(
(server, _, done) => {
server.get(PATH, async (req, res) => {
return req.oauthHandle(res, 'OIDC', oidcOauth);
});
done();
},
{ name: PATH },
);

View File

@@ -8,7 +8,7 @@ import { administratorMiddleware } from '@/server/middleware/administrator';
import { userMiddleware } from '@/server/middleware/user';
import fastifyPlugin from 'fastify-plugin';
import { statSync } from 'fs';
import ms from 'ms';
import ms, { StringValue } from 'ms';
import { cpus } from 'os';
import { resolve } from 'path';
import { z } from 'zod';
@@ -20,7 +20,7 @@ type Body = Partial<Settings>;
const reservedRoutes = ['/dashboard', '/api', '/raw', '/robots.txt', '/manifest.json', '/favicon.ico'];
const zMs = z.string().refine((value) => ms(value) > 0, 'Value must be greater than 0');
const zMs = z.string().refine((value) => ms(value as StringValue) > 0, 'Value must be greater than 0');
const zBytes = z.string().refine((value) => bytes(value) > 0, 'Value must be greater than 0');
const discordEmbed = z

View File

@@ -1,6 +1,5 @@
import { config } from '@/lib/config';
import { prisma } from '@/lib/db';
import { User } from '@/lib/db/models/user';
import { randomCharacters } from '@/lib/random';
import { FastifyReply, FastifyRequest } from 'fastify';
import { IncomingMessage, ServerResponse } from 'http';
@@ -54,7 +53,7 @@ export async function getSession(
export async function saveSession(
session: Awaited<ReturnType<typeof getSession>>,
user: User,
user: { id: string } & Record<string, any>,
overwriteSessions = true,
) {
session.id = user.id;