mirror of
https://github.com/diced/zipline.git
synced 2026-04-28 10:43:06 -07:00
feat: overhaul oauth + PKCE for OIDC
refactored a lot of oauth stuff, so there may be bugs
This commit is contained in:
@@ -61,11 +61,14 @@ export const API_ERRORS = {
|
|||||||
1060: 'Passkey has legacy registration data and cannot be used',
|
1060: 'Passkey has legacy registration data and cannot be used',
|
||||||
1061: 'Invalid multipart/form-data request',
|
1061: 'Invalid multipart/form-data request',
|
||||||
1062: 'No files in multipart/form-data request',
|
1062: 'No files in multipart/form-data request',
|
||||||
|
1063: 'Already linked to this OAuth provider',
|
||||||
|
1064: 'Invalid OAuth state parameter',
|
||||||
|
|
||||||
// 2xxx, session errors
|
// 2xxx, session errors
|
||||||
2000: 'Invalid login session',
|
2000: 'Invalid login session',
|
||||||
2001: 'Invalid token',
|
2001: 'Invalid token',
|
||||||
2002: 'Not logged in',
|
2002: 'Not logged in',
|
||||||
|
2003: 'OAuth provider is not configured (or misconfigured)',
|
||||||
|
|
||||||
// 3xxx, permission errors
|
// 3xxx, permission errors
|
||||||
3000: 'Admin only',
|
3000: 'Admin only',
|
||||||
@@ -84,6 +87,8 @@ export const API_ERRORS = {
|
|||||||
3013: "You don't have permission to delete the selected files",
|
3013: "You don't have permission to delete the selected files",
|
||||||
3014: "You don't have permission to modify the selected files",
|
3014: "You don't have permission to modify the selected files",
|
||||||
3015: 'Not super admin',
|
3015: 'Not super admin',
|
||||||
|
3016: 'OAuth registration is disabled',
|
||||||
|
3017: 'OAuth login is not allowed for this account',
|
||||||
|
|
||||||
// 4xxx, not founds
|
// 4xxx, not founds
|
||||||
4000: 'File not found',
|
4000: 'File not found',
|
||||||
@@ -109,6 +114,13 @@ export const API_ERRORS = {
|
|||||||
6001: 'Failed to fetch version details',
|
6001: 'Failed to fetch version details',
|
||||||
6002: 'Failed to rename file in datasource',
|
6002: 'Failed to rename file in datasource',
|
||||||
6003: 'There was an error during a healthcheck',
|
6003: 'There was an error during a healthcheck',
|
||||||
|
6004: 'Failed to fetch OAuth access token',
|
||||||
|
6005: 'No access token in OAuth response',
|
||||||
|
6006: 'No refresh token in OAuth response',
|
||||||
|
6007: 'Failed to fetch OAuth user',
|
||||||
|
6008: 'OAuth provider request failed',
|
||||||
|
6009: "Couldn't create user via OAuth profile",
|
||||||
|
6010: 'The username is already taken by another account',
|
||||||
|
|
||||||
// 9xxx catch all
|
// 9xxx catch all
|
||||||
9000: 'Bad request',
|
9000: 'Bad request',
|
||||||
@@ -128,14 +140,16 @@ export type ApiErrorPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
public readonly code: ApiErrorCode;
|
|
||||||
public readonly status: number;
|
public readonly status: number;
|
||||||
public additional: Record<string, any>;
|
public additional: Record<string, any>;
|
||||||
|
|
||||||
constructor(code: ApiErrorCode, message?: string, status?: number) {
|
constructor(
|
||||||
|
public readonly code: ApiErrorCode,
|
||||||
|
message?: string,
|
||||||
|
status?: number,
|
||||||
|
) {
|
||||||
super(message ?? API_ERRORS[code] ?? 'Unknown API error');
|
super(message ?? API_ERRORS[code] ?? 'Unknown API error');
|
||||||
|
|
||||||
this.code = code;
|
|
||||||
this.status = status ?? ApiError.codeToHttpStatus(code);
|
this.status = status ?? ApiError.codeToHttpStatus(code);
|
||||||
this.additional = {} as Record<string, any>;
|
this.additional = {} as Record<string, any>;
|
||||||
|
|
||||||
@@ -184,3 +198,11 @@ export class ApiError extends Error {
|
|||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RedirectError extends Error {
|
||||||
|
constructor(public readonly url: string) {
|
||||||
|
super('Redirect');
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
9
src/lib/oauth/pkce.ts
Normal file
9
src/lib/oauth/pkce.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createHash, randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
export function generatePKCEVerifier(size = 32): string {
|
||||||
|
return randomBytes(size).toString('base64url');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePKCEChallenge(verifier: string): string {
|
||||||
|
return createHash('sha256').update(verifier).digest('base64url');
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { OAuthProviderType } from '@/prisma/client';
|
|
||||||
import { User } from '../db/models/user';
|
|
||||||
|
|
||||||
export function findProvider(
|
|
||||||
provider: OAuthProviderType,
|
|
||||||
providers: User['oauthProviders'],
|
|
||||||
): User['oauthProviders'][0] | undefined {
|
|
||||||
return providers.find((p) => p.provider === provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const githubAuth = {
|
|
||||||
url: (clientId: string, state?: string, redirectUri?: string) =>
|
|
||||||
`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=read:user${
|
|
||||||
state ? `&state=${encodeURIComponent(state)}` : ''
|
|
||||||
}${redirectUri ? `&redirect_uri=${encodeURIComponent(redirectUri)}` : ''}`,
|
|
||||||
user: async (accessToken: string) => {
|
|
||||||
const res = await fetch('https://api.github.com/user', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const discordAuth = {
|
|
||||||
url: (clientId: string, origin: string, state?: string, redirectUri?: string) =>
|
|
||||||
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
|
||||||
redirectUri ?? `${origin}/api/auth/oauth/discord`,
|
|
||||||
)}&response_type=code&scope=identify&prompt=none${state ? `&state=${encodeURIComponent(state)}` : ''}`,
|
|
||||||
user: async (accessToken: string) => {
|
|
||||||
const res = await fetch('https://discord.com/api/users/@me', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const googleAuth = {
|
|
||||||
url: (clientId: string, origin: string, state?: string, redirectUri?: string) =>
|
|
||||||
`https://accounts.google.com/o/oauth2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
|
||||||
redirectUri ?? `${origin}/api/auth/oauth/google`,
|
|
||||||
)}&response_type=code&access_type=offline&scope=https://www.googleapis.com/auth/userinfo.profile${
|
|
||||||
state ? `&state=${encodeURIComponent(state)}` : ''
|
|
||||||
}`,
|
|
||||||
user: async (accessToken: string) => {
|
|
||||||
const res = await fetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const oidcAuth = {
|
|
||||||
url: (clientId: string, origin: string, authorizeUrl: string, state?: string, redirectUri?: string) =>
|
|
||||||
`${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
|
||||||
redirectUri ?? `${origin}/api/auth/oauth/oidc`,
|
|
||||||
)}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${encodeURIComponent(state)}` : ''}`,
|
|
||||||
user: async (accessToken: string, userInfoUrl: string) => {
|
|
||||||
const res = await fetch(userInfoUrl, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
22
src/lib/oauth/providers/discord.ts
Normal file
22
src/lib/oauth/providers/discord.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { fetchUserInfo, type OAuthOptions, type OAuthUserInfoOptions } from '.';
|
||||||
|
|
||||||
|
export function discordAuthorizeURL({ clientId, origin, state, redirectUri }: OAuthOptions): string {
|
||||||
|
const u = new URL('https://discord.com/api/oauth2/authorize');
|
||||||
|
|
||||||
|
u.searchParams.set('client_id', clientId);
|
||||||
|
u.searchParams.set('redirect_uri', redirectUri ?? `${origin}/api/auth/oauth/discord`);
|
||||||
|
u.searchParams.set('response_type', 'code');
|
||||||
|
u.searchParams.set('scope', 'identify');
|
||||||
|
u.searchParams.set('prompt', 'none');
|
||||||
|
|
||||||
|
if (state) u.searchParams.set('state', state);
|
||||||
|
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function discordUser(options: OAuthUserInfoOptions) {
|
||||||
|
return fetchUserInfo({
|
||||||
|
userInfoUrl: 'https://discord.com/api/users/@me',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
src/lib/oauth/providers/github.ts
Normal file
20
src/lib/oauth/providers/github.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { fetchUserInfo, type OAuthOptions, type OAuthUserInfoOptions } from '.';
|
||||||
|
|
||||||
|
export function githubAuthorizeURL({ clientId, state, redirectUri, origin }: OAuthOptions): string {
|
||||||
|
const u = new URL('https://github.com/login/oauth/authorize');
|
||||||
|
|
||||||
|
u.searchParams.set('client_id', clientId);
|
||||||
|
u.searchParams.set('redirect_uri', redirectUri ?? `${origin}/api/auth/oauth/github`);
|
||||||
|
u.searchParams.set('scope', 'read:user');
|
||||||
|
|
||||||
|
if (state) u.searchParams.set('state', state);
|
||||||
|
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function githubUser(options: OAuthUserInfoOptions) {
|
||||||
|
return fetchUserInfo({
|
||||||
|
userInfoUrl: 'https://api.github.com/user',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/lib/oauth/providers/google.ts
Normal file
22
src/lib/oauth/providers/google.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { fetchUserInfo, type OAuthUserInfoOptions, type OAuthOptions } from '.';
|
||||||
|
|
||||||
|
export function googleAuthorizeURL({ clientId, origin, state, redirectUri }: OAuthOptions): string {
|
||||||
|
const u = new URL('https://accounts.google.com/o/oauth2/auth');
|
||||||
|
|
||||||
|
u.searchParams.set('client_id', clientId);
|
||||||
|
u.searchParams.set('redirect_uri', redirectUri ?? `${origin}/api/auth/oauth/google`);
|
||||||
|
u.searchParams.set('response_type', 'code');
|
||||||
|
u.searchParams.set('access_type', 'offline');
|
||||||
|
u.searchParams.set('scope', 'https://www.googleapis.com/auth/userinfo.profile');
|
||||||
|
|
||||||
|
if (state) u.searchParams.set('state', state);
|
||||||
|
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function googleUser(options: OAuthUserInfoOptions) {
|
||||||
|
return fetchUserInfo({
|
||||||
|
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
40
src/lib/oauth/providers/index.ts
Normal file
40
src/lib/oauth/providers/index.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { OAuthProviderType } from '@/prisma/client';
|
||||||
|
import { User } from '../../db/models/user';
|
||||||
|
|
||||||
|
export function findProvider(
|
||||||
|
provider: OAuthProviderType,
|
||||||
|
providers: User['oauthProviders'],
|
||||||
|
): User['oauthProviders'][0] | undefined {
|
||||||
|
return providers.find((p) => p.provider === provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUserInfo({ userInfoUrl, accessToken }: OAuthUserInfoOptions): Promise<any | null> {
|
||||||
|
const res = await fetch(userInfoUrl!, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OAuthOptions = {
|
||||||
|
clientId: string;
|
||||||
|
origin: string;
|
||||||
|
state?: string;
|
||||||
|
redirectUri: string;
|
||||||
|
|
||||||
|
authorizeUrl?: string;
|
||||||
|
|
||||||
|
codeChallenge?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OAuthUserInfoOptions = {
|
||||||
|
accessToken: string;
|
||||||
|
userInfoUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { discordAuthorizeURL, discordUser } from './discord';
|
||||||
|
export { githubAuthorizeURL, githubUser } from './github';
|
||||||
|
export { googleAuthorizeURL, googleUser } from './google';
|
||||||
|
export { oidcAuthorizeURL, oidcUser } from './oidc';
|
||||||
32
src/lib/oauth/providers/oidc.ts
Normal file
32
src/lib/oauth/providers/oidc.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiError } from '@/lib/api/errors';
|
||||||
|
import { type OAuthOptions, type OAuthUserInfoOptions, fetchUserInfo } from '.';
|
||||||
|
|
||||||
|
export function oidcAuthorizeURL({
|
||||||
|
authorizeUrl,
|
||||||
|
clientId,
|
||||||
|
origin,
|
||||||
|
state,
|
||||||
|
redirectUri,
|
||||||
|
codeChallenge,
|
||||||
|
}: OAuthOptions): string {
|
||||||
|
if (!authorizeUrl) throw new ApiError(2003);
|
||||||
|
|
||||||
|
const u = new URL(authorizeUrl);
|
||||||
|
|
||||||
|
u.searchParams.set('client_id', clientId);
|
||||||
|
u.searchParams.set('redirect_uri', redirectUri ?? `${origin}/api/auth/oauth/oidc`);
|
||||||
|
u.searchParams.set('response_type', 'code');
|
||||||
|
u.searchParams.set('scope', 'openid email profile offline_access');
|
||||||
|
|
||||||
|
if (state) u.searchParams.set('state', state);
|
||||||
|
if (codeChallenge) {
|
||||||
|
u.searchParams.set('code_challenge_method', 'S256');
|
||||||
|
u.searchParams.set('code_challenge', codeChallenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oidcUser(options: OAuthUserInfoOptions) {
|
||||||
|
return fetchUserInfo(options);
|
||||||
|
}
|
||||||
40
src/lib/oauth/state.ts
Normal file
40
src/lib/oauth/state.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { config } from '@/lib/config';
|
||||||
|
import { decrypt, encrypt } from '@/lib/crypto';
|
||||||
|
|
||||||
|
export type OAuthStateJSON = {
|
||||||
|
mode: 'default' | 'link';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function encryptOAuthState(value: OAuthStateJSON): string {
|
||||||
|
return encrypt(JSON.stringify(value), config.core.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptOAuthState(state?: string): string | null {
|
||||||
|
if (!state) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decrypt(decodeURIComponent(state), config.core.secret);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseOAuthState(state?: string): OAuthStateJSON | null {
|
||||||
|
const decrypted = decryptOAuthState(state);
|
||||||
|
if (!decrypted) return null;
|
||||||
|
|
||||||
|
// legacy
|
||||||
|
if (decrypted === 'link') return { mode: 'link' };
|
||||||
|
if (decrypted === 'default') return { mode: 'default' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(decrypted) as Partial<OAuthStateJSON>;
|
||||||
|
if (parsed?.mode !== 'default' && parsed?.mode !== 'link') return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: parsed.mode,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,8 +63,8 @@ async function main() {
|
|||||||
}).withTypeProvider<ZodTypeProvider>();
|
}).withTypeProvider<ZodTypeProvider>();
|
||||||
|
|
||||||
await registerPlugins(server);
|
await registerPlugins(server);
|
||||||
await registerRoutes(server, MODE);
|
|
||||||
registerHandlers(server, MODE);
|
registerHandlers(server, MODE);
|
||||||
|
await registerRoutes(server, MODE);
|
||||||
|
|
||||||
if (process.env.ZIPLINE_OUTPUT_OPENAPI === 'true') generateOpenApiSpec(server);
|
if (process.env.ZIPLINE_OUTPUT_OPENAPI === 'true') generateOpenApiSpec(server);
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { createToken, decrypt } from '@/lib/crypto';
|
import { createToken } from '@/lib/crypto';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import Logger, { log } from '@/lib/logger';
|
import Logger, { log } from '@/lib/logger';
|
||||||
import { findProvider } from '@/lib/oauth/providers';
|
import { findProvider } from '@/lib/oauth/providers';
|
||||||
import { OAuthProviderType, User } from '@/prisma/client';
|
import { OAuthProviderType, User } from '@/prisma/client';
|
||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
import { getSession, saveSession } from '../session';
|
import { getSession, saveSession, ZiplineIronSession } from '../session';
|
||||||
|
import { parseOAuthState } from '@/lib/oauth/state';
|
||||||
|
import { ApiError } from '@/lib/api/errors';
|
||||||
|
|
||||||
export type OAuthQuery = {
|
export type OAuthQuery = {
|
||||||
state?: string;
|
state?: string;
|
||||||
code: string;
|
code: string;
|
||||||
host: string;
|
host: string;
|
||||||
|
session: ZiplineIronSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OAuthResponse = {
|
export type OAuthResponse = {
|
||||||
username?: string;
|
username: string;
|
||||||
user_id?: string;
|
user_id: string;
|
||||||
access_token?: string;
|
access_token: string;
|
||||||
refresh_token?: string;
|
refresh_token?: string | null;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
|
|
||||||
error?: string;
|
|
||||||
error_code?: number;
|
|
||||||
redirect?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function oauthPlugin(fastify: FastifyInstance) {
|
async function oauthPlugin(fastify: FastifyInstance) {
|
||||||
@@ -36,23 +35,17 @@ async function oauthPlugin(fastify: FastifyInstance) {
|
|||||||
handler: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
|
handler: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
|
||||||
) {
|
) {
|
||||||
const logger = log('api').c('auth').c('oauth').c(provider.toLowerCase());
|
const logger = log('api').c('auth').c('oauth').c(provider.toLowerCase());
|
||||||
|
|
||||||
(this.query as any).host = this.headers.host ?? 'localhost:3000';
|
|
||||||
|
|
||||||
const response = await handler(this.query as OAuthQuery, logger);
|
|
||||||
const session = await getSession(this, reply);
|
const session = await getSession(this, reply);
|
||||||
|
|
||||||
if (response.error) {
|
const q = this.query as { state?: string; code?: string };
|
||||||
logger.warn('invalid oauth request', {
|
const query: OAuthQuery = {
|
||||||
error: response.error,
|
state: q.state,
|
||||||
});
|
code: q.code ?? '',
|
||||||
|
host: this.headers.host ?? 'localhost:3000',
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
|
||||||
return reply.internalServerError(response.error);
|
const response = await handler(query, logger);
|
||||||
}
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
return reply.redirect(response.redirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('oauth response', {
|
logger.debug('oauth response', {
|
||||||
response,
|
response,
|
||||||
@@ -77,7 +70,8 @@ async function oauthPlugin(fastify: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { state } = this.query as OAuthQuery;
|
const state = parseOAuthState(query.state);
|
||||||
|
if (!state) throw new ApiError(1064);
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -94,18 +88,10 @@ async function oauthPlugin(fastify: FastifyInstance) {
|
|||||||
|
|
||||||
const userOauth = findProvider(provider, user?.oauthProviders ?? []);
|
const userOauth = findProvider(provider, user?.oauthProviders ?? []);
|
||||||
|
|
||||||
let urlState;
|
if (state.mode === 'link') {
|
||||||
try {
|
if (!user) throw new ApiError(2000);
|
||||||
urlState = decrypt(decodeURIComponent(state ?? ''), config.core.secret);
|
|
||||||
} catch {
|
|
||||||
urlState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlState === 'link') {
|
if (findProvider(provider, user.oauthProviders)) throw new ApiError(1063);
|
||||||
if (!user) return reply.unauthorized('invalid session');
|
|
||||||
|
|
||||||
if (findProvider(provider, user.oauthProviders))
|
|
||||||
return reply.badRequest('This account is already linked to this provider');
|
|
||||||
|
|
||||||
logger.debug('attempting to link oauth account', {
|
logger.debug('attempting to link oauth account', {
|
||||||
provider,
|
provider,
|
||||||
@@ -145,7 +131,7 @@ async function oauthPlugin(fastify: FastifyInstance) {
|
|||||||
error: e,
|
error: e,
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply.badRequest('Cant link account, already linked with this provider');
|
throw new ApiError(1063);
|
||||||
}
|
}
|
||||||
} else if (user && userOauth) {
|
} else if (user && userOauth) {
|
||||||
await prisma.oAuthProvider.update({
|
await prisma.oAuthProvider.update({
|
||||||
@@ -199,9 +185,10 @@ async function oauthPlugin(fastify: FastifyInstance) {
|
|||||||
oauth: response.username || 'unknown',
|
oauth: response.username || 'unknown',
|
||||||
ua: this.headers['user-agent'],
|
ua: this.headers['user-agent'],
|
||||||
});
|
});
|
||||||
return reply.badRequest("Can't create users through oauth.");
|
|
||||||
|
throw new ApiError(6009);
|
||||||
} else if (existingUser) {
|
} else if (existingUser) {
|
||||||
return reply.badRequest('This username is already taken');
|
throw new ApiError(6010);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -242,7 +229,7 @@ async function oauthPlugin(fastify: FastifyInstance) {
|
|||||||
response,
|
response,
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply.badRequest('Cant create user, already linked with this provider');
|
throw new ApiError(1063);
|
||||||
} else throw e;
|
} else throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,29 @@
|
|||||||
|
import { ApiError, RedirectError } from '@/lib/api/errors';
|
||||||
import { fetchToDataURL } from '@/lib/base64';
|
import { fetchToDataURL } from '@/lib/base64';
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { encrypt } from '@/lib/crypto';
|
|
||||||
import Logger from '@/lib/logger';
|
import Logger from '@/lib/logger';
|
||||||
import enabled from '@/lib/oauth/enabled';
|
import enabled from '@/lib/oauth/enabled';
|
||||||
import { discordAuth } from '@/lib/oauth/providers';
|
import { encryptOAuthState } from '@/lib/oauth/state';
|
||||||
|
import { discordAuthorizeURL, discordUser } from '@/lib/oauth/providers';
|
||||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||||
import typedPlugin from '@/server/typedPlugin';
|
import typedPlugin from '@/server/typedPlugin';
|
||||||
|
|
||||||
async function discordOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
async function discordOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||||
if (!config.features.oauthRegistration)
|
if (!config.features.oauthRegistration) throw new ApiError(3016);
|
||||||
return {
|
|
||||||
error: 'OAuth registration is disabled.',
|
|
||||||
error_code: 403,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { discord: discordEnabled } = enabled(config);
|
const { discord: discordEnabled } = enabled(config);
|
||||||
|
|
||||||
if (!discordEnabled)
|
if (!discordEnabled) throw new ApiError(2003, 'Discord OAuth is not configured.');
|
||||||
return {
|
|
||||||
error: 'Discord OAuth is not configured.',
|
|
||||||
error_code: 401,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const linkState = encrypt('link', config.core.secret);
|
throw new RedirectError(
|
||||||
|
discordAuthorizeURL({
|
||||||
return {
|
clientId: config.oauth.discord.clientId!,
|
||||||
redirect: discordAuth.url(
|
origin: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
||||||
config.oauth.discord.clientId!,
|
state: encryptOAuthState({ mode: state === 'link' ? 'link' : 'default' }),
|
||||||
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
redirectUri: config.oauth.discord.redirectUri!,
|
||||||
state === 'link' ? linkState : undefined,
|
}),
|
||||||
config.oauth.discord.redirectUri ?? undefined,
|
);
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
@@ -65,29 +56,26 @@ async function discordOauth({ code, host, state }: OAuthQuery, logger: Logger):
|
|||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
throw new ApiError(6004);
|
||||||
error: 'Failed to fetch access token',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
if (!json.access_token) return { error: 'No access token in response' };
|
if (!json.access_token) throw new ApiError(6005);
|
||||||
if (!json.refresh_token) return { error: 'No refresh token in response' };
|
if (!json.refresh_token) throw new ApiError(6006);
|
||||||
|
|
||||||
const userJson = await discordAuth.user(json.access_token);
|
const userJson = await discordUser({
|
||||||
if (!userJson) return { error: 'Failed to fetch user' };
|
accessToken: json.access_token,
|
||||||
|
});
|
||||||
|
if (!userJson) throw new ApiError(6007);
|
||||||
|
|
||||||
logger.debug('user', { '@me': userJson });
|
logger.debug('user', { '@me': userJson });
|
||||||
|
|
||||||
const allowedIds = config.oauth.discord.allowedIds;
|
const allowedIds = config.oauth.discord.allowedIds;
|
||||||
const deniedIds = config.oauth.discord.deniedIds;
|
const deniedIds = config.oauth.discord.deniedIds;
|
||||||
if (deniedIds && deniedIds.length > 0 && deniedIds.includes(userJson.id)) {
|
if (deniedIds && deniedIds.length > 0 && deniedIds.includes(userJson.id)) throw new ApiError(3017);
|
||||||
return { error: 'You are not allowed to log in with Discord.' };
|
|
||||||
}
|
if (allowedIds && allowedIds.length > 0 && !allowedIds.includes(userJson.id)) throw new ApiError(3017);
|
||||||
if (allowedIds && allowedIds.length > 0 && !allowedIds.includes(userJson.id)) {
|
|
||||||
return { error: 'You are not allowed to log in with Discord.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatar = userJson.avatar
|
const avatar = userJson.avatar
|
||||||
? `https://cdn.discordapp.com/avatars/${userJson.id}/${userJson.avatar}.png`
|
? `https://cdn.discordapp.com/avatars/${userJson.id}/${userJson.avatar}.png`
|
||||||
|
|||||||
@@ -1,37 +1,29 @@
|
|||||||
|
import { ApiError, RedirectError } from '@/lib/api/errors';
|
||||||
import { fetchToDataURL } from '@/lib/base64';
|
import { fetchToDataURL } from '@/lib/base64';
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { encrypt } from '@/lib/crypto';
|
|
||||||
import Logger from '@/lib/logger';
|
import Logger from '@/lib/logger';
|
||||||
import enabled from '@/lib/oauth/enabled';
|
import enabled from '@/lib/oauth/enabled';
|
||||||
import { githubAuth } from '@/lib/oauth/providers';
|
import { githubAuthorizeURL, githubUser } from '@/lib/oauth/providers';
|
||||||
|
import { encryptOAuthState } from '@/lib/oauth/state';
|
||||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||||
import typedPlugin from '@/server/typedPlugin';
|
import typedPlugin from '@/server/typedPlugin';
|
||||||
|
|
||||||
async function githubOauth({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
async function githubOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||||
if (!config.features.oauthRegistration)
|
if (!config.features.oauthRegistration) throw new ApiError(3016);
|
||||||
return {
|
|
||||||
error: 'OAuth registration is disabled.',
|
|
||||||
error_code: 403,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { github: githubEnabled } = enabled(config);
|
const { github: githubEnabled } = enabled(config);
|
||||||
|
|
||||||
if (!githubEnabled)
|
if (!githubEnabled) throw new ApiError(2003, 'GitHub OAuth is not configured.');
|
||||||
return {
|
|
||||||
error: 'GitHub OAuth is not configured.',
|
|
||||||
error_code: 401,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const linkState = encrypt('link', config.core.secret);
|
throw new RedirectError(
|
||||||
|
githubAuthorizeURL({
|
||||||
return {
|
clientId: config.oauth.github.clientId!,
|
||||||
redirect: githubAuth.url(
|
state: encryptOAuthState({ mode: state === 'link' ? 'link' : 'default' }),
|
||||||
config.oauth.github.clientId!,
|
redirectUri: config.oauth.github.redirectUri!,
|
||||||
state === 'link' ? linkState : undefined,
|
origin: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
||||||
config.oauth.github.redirectUri ?? undefined,
|
}),
|
||||||
),
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
@@ -55,10 +47,7 @@ async function githubOauth({ code, state }: OAuthQuery, logger: Logger): Promise
|
|||||||
|
|
||||||
const isJson = res.headers.get('content-type')?.startsWith('application/json');
|
const isJson = res.headers.get('content-type')?.startsWith('application/json');
|
||||||
|
|
||||||
if (!isJson && !res.ok)
|
if (!isJson && !res.ok) throw new ApiError(6004);
|
||||||
return {
|
|
||||||
error: 'Failed to fetch access token',
|
|
||||||
};
|
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
@@ -68,13 +57,15 @@ async function githubOauth({ code, state }: OAuthQuery, logger: Logger): Promise
|
|||||||
});
|
});
|
||||||
logger.debug('failed to fetch access token', { json, status: res.status });
|
logger.debug('failed to fetch access token', { json, status: res.status });
|
||||||
|
|
||||||
return { error: 'there was an error while processing github request' };
|
throw new ApiError(6008);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json.access_token) return { error: 'No access token in response' };
|
if (!json.access_token) throw new ApiError(6005);
|
||||||
|
|
||||||
const userJson = await githubAuth.user(json.access_token);
|
const userJson = await githubUser({
|
||||||
if (!userJson) return { error: 'Failed to fetch user' };
|
accessToken: json.access_token,
|
||||||
|
});
|
||||||
|
if (!userJson) throw new ApiError(6007);
|
||||||
|
|
||||||
logger.debug('user', { user: userJson });
|
logger.debug('user', { user: userJson });
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,29 @@
|
|||||||
|
import { ApiError, RedirectError } from '@/lib/api/errors';
|
||||||
import { fetchToDataURL } from '@/lib/base64';
|
import { fetchToDataURL } from '@/lib/base64';
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { encrypt } from '@/lib/crypto';
|
|
||||||
import Logger from '@/lib/logger';
|
import Logger from '@/lib/logger';
|
||||||
import enabled from '@/lib/oauth/enabled';
|
import enabled from '@/lib/oauth/enabled';
|
||||||
import { googleAuth } from '@/lib/oauth/providers';
|
import { encryptOAuthState } from '@/lib/oauth/state';
|
||||||
|
import { googleAuthorizeURL, googleUser } from '@/lib/oauth/providers';
|
||||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||||
import typedPlugin from '@/server/typedPlugin';
|
import typedPlugin from '@/server/typedPlugin';
|
||||||
|
|
||||||
async function googleOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
async function googleOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||||
if (!config.features.oauthRegistration)
|
if (!config.features.oauthRegistration) throw new ApiError(3016);
|
||||||
return {
|
|
||||||
error: 'OAuth registration is disabled.',
|
|
||||||
error_code: 403,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { google: googleEnabled } = enabled(config);
|
const { google: googleEnabled } = enabled(config);
|
||||||
|
|
||||||
if (!googleEnabled)
|
if (!googleEnabled) throw new ApiError(2003, 'Google OAuth is not configured.');
|
||||||
return {
|
|
||||||
error: 'Google OAuth is not configured.',
|
|
||||||
error_code: 401,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const linkState = encrypt('link', config.core.secret);
|
throw new RedirectError(
|
||||||
|
googleAuthorizeURL({
|
||||||
return {
|
clientId: config.oauth.google.clientId!,
|
||||||
redirect: googleAuth.url(
|
origin: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
||||||
config.oauth.google.clientId!,
|
state: encryptOAuthState({ mode: state === 'link' ? 'link' : 'default' }),
|
||||||
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
redirectUri: config.oauth.google.redirectUri!,
|
||||||
state === 'link' ? linkState : undefined,
|
}),
|
||||||
config.oauth.google.redirectUri ?? undefined,
|
);
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
@@ -63,16 +54,16 @@ async function googleOauth({ code, host, state }: OAuthQuery, logger: Logger): P
|
|||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
throw new ApiError(6004);
|
||||||
error: 'Failed to fetch access token',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.access_token) return { error: 'No access token in response' };
|
if (!json.access_token) throw new ApiError(6005);
|
||||||
|
|
||||||
const userJson = await googleAuth.user(json.access_token);
|
const userJson = await googleUser({
|
||||||
if (!userJson) return { error: 'Failed to fetch user' };
|
accessToken: json.access_token,
|
||||||
|
});
|
||||||
|
if (!userJson) throw new ApiError(6007);
|
||||||
|
|
||||||
logger.debug('user', { userinfo: userJson });
|
logger.debug('user', { userinfo: userJson });
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,44 @@
|
|||||||
|
import { ApiError, RedirectError } from '@/lib/api/errors';
|
||||||
import { fetchToDataURL } from '@/lib/base64';
|
import { fetchToDataURL } from '@/lib/base64';
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { encrypt } from '@/lib/crypto';
|
|
||||||
import Logger from '@/lib/logger';
|
import Logger from '@/lib/logger';
|
||||||
import enabled from '@/lib/oauth/enabled';
|
import enabled from '@/lib/oauth/enabled';
|
||||||
import { oidcAuth } from '@/lib/oauth/providers';
|
import { generatePKCEChallenge, generatePKCEVerifier } from '@/lib/oauth/pkce';
|
||||||
|
import { oidcAuthorizeURL, oidcUser } from '@/lib/oauth/providers';
|
||||||
|
import { encryptOAuthState } from '@/lib/oauth/state';
|
||||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||||
import typedPlugin from '@/server/typedPlugin';
|
import typedPlugin from '@/server/typedPlugin';
|
||||||
|
|
||||||
async function oidcOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
async function oidcOauth({ code, host, state, session }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||||
if (!config.features.oauthRegistration)
|
if (!config.features.oauthRegistration) throw new ApiError(3016);
|
||||||
return {
|
|
||||||
error: 'OAuth registration is disabled.',
|
|
||||||
error_code: 403,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { oidc: oidcEnabled } = enabled(config);
|
const { oidc: oidcEnabled } = enabled(config);
|
||||||
|
|
||||||
if (!oidcEnabled)
|
if (!oidcEnabled) throw new ApiError(2003, 'OpenID Connect OAuth is not configured.');
|
||||||
return {
|
|
||||||
error: 'OpenID Connect OAuth is not configured.',
|
|
||||||
error_code: 401,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const linkState = encrypt('link', config.core.secret);
|
const pkceVerifier = generatePKCEVerifier();
|
||||||
const defaultState = encrypt('default', config.core.secret);
|
const codeChallenge = generatePKCEChallenge(pkceVerifier);
|
||||||
|
|
||||||
return {
|
session.pkceVerifier = pkceVerifier;
|
||||||
redirect: oidcAuth.url(
|
await session.save();
|
||||||
config.oauth.oidc.clientId!,
|
|
||||||
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
throw new RedirectError(
|
||||||
config.oauth.oidc.authorizeUrl!,
|
oidcAuthorizeURL({
|
||||||
state === 'link' ? linkState : defaultState,
|
clientId: config.oauth.oidc.clientId!,
|
||||||
config.oauth.oidc.redirectUri ?? undefined,
|
origin: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
||||||
),
|
state: encryptOAuthState({ mode: state === 'link' ? 'link' : 'default' }),
|
||||||
};
|
redirectUri: config.oauth.oidc.redirectUri!,
|
||||||
|
authorizeUrl: config.oauth.oidc.authorizeUrl!,
|
||||||
|
codeChallenge,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkceVerifier = session.pkceVerifier;
|
||||||
|
if (pkceVerifier) {
|
||||||
|
delete session.pkceVerifier;
|
||||||
|
await session.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
@@ -46,6 +50,9 @@ async function oidcOauth({ code, host, state }: OAuthQuery, logger: Logger): Pro
|
|||||||
config.oauth.oidc.redirectUri ??
|
config.oauth.oidc.redirectUri ??
|
||||||
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/oidc`,
|
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/oidc`,
|
||||||
});
|
});
|
||||||
|
if (pkceVerifier) {
|
||||||
|
body.set('code_verifier', pkceVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('oidc oauth request', {
|
logger.debug('oidc oauth request', {
|
||||||
body: body.toString(),
|
body: body.toString(),
|
||||||
@@ -63,16 +70,17 @@ async function oidcOauth({ code, host, state }: OAuthQuery, logger: Logger): Pro
|
|||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
logger.debug('oidc oauth failed with a non 200 status code', { status: res.status, text });
|
logger.debug('oidc oauth failed with a non 200 status code', { status: res.status, text });
|
||||||
|
|
||||||
return {
|
throw new ApiError(6004);
|
||||||
error: 'Failed to fetch access token',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.access_token) return { error: 'No access token in response' };
|
if (!json.access_token) throw new ApiError(6005);
|
||||||
|
|
||||||
const userJson = await oidcAuth.user(json.access_token, config.oauth.oidc.userinfoUrl!);
|
const userJson = await oidcUser({
|
||||||
if (!userJson) return { error: 'Failed to fetch user' };
|
accessToken: json.access_token,
|
||||||
|
userInfoUrl: config.oauth.oidc.userinfoUrl!,
|
||||||
|
});
|
||||||
|
if (!userJson) throw new ApiError(6007);
|
||||||
|
|
||||||
logger.debug('user', { userinfo: userJson });
|
logger.debug('user', { userinfo: userJson });
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ export type ZiplineSession = {
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
client: ZiplineClient;
|
client: ZiplineClient;
|
||||||
|
|
||||||
|
pkceVerifier?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ZiplineIronSession = Awaited<ReturnType<typeof getSession>>;
|
||||||
|
|
||||||
export async function getSession(
|
export async function getSession(
|
||||||
req: FastifyRequest | IncomingMessage,
|
req: FastifyRequest | IncomingMessage,
|
||||||
reply: FastifyReply | ServerResponse<IncomingMessage>,
|
reply: FastifyReply | ServerResponse<IncomingMessage>,
|
||||||
@@ -48,7 +52,7 @@ export async function getSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSession(
|
export async function saveSession(
|
||||||
session: Awaited<ReturnType<typeof getSession>>,
|
session: ZiplineIronSession,
|
||||||
user: { id: string } & Record<string, any>,
|
user: { id: string } & Record<string, any>,
|
||||||
overwriteSessions = true,
|
overwriteSessions = true,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiError } from '@/lib/api/errors';
|
import { ApiError, RedirectError } from '@/lib/api/errors';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import { hasZodFastifySchemaValidationErrors, isResponseSerializationError } from 'fastify-type-provider-zod';
|
import { hasZodFastifySchemaValidationErrors, isResponseSerializationError } from 'fastify-type-provider-zod';
|
||||||
|
|
||||||
@@ -44,6 +44,10 @@ export function registerHandlers(server: FastifyInstance, mode: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof RedirectError) {
|
||||||
|
return res.redirect(error.url);
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof ApiError) {
|
if (error instanceof ApiError) {
|
||||||
const apiError = error as ApiError;
|
const apiError = error as ApiError;
|
||||||
return res.status(apiError.status).send(apiError.toJSON());
|
return res.status(apiError.status).send(apiError.toJSON());
|
||||||
|
|||||||
Reference in New Issue
Block a user