fix: sessions & authentik -> oidc & #603

This commit is contained in:
diced 2024-09-04 22:55:15 -07:00
parent 708b130002
commit 67a7d44198
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
20 changed files with 119 additions and 107 deletions

View file

@ -118,11 +118,11 @@ OAUTH_GITHUB_CLIENT_SECRET=x
OAUTH_GOOGLE_CLIENT_ID=x-x.apps.googleusercontent.com OAUTH_GOOGLE_CLIENT_ID=x-x.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=x-x-x OAUTH_GOOGLE_CLIENT_SECRET=x-x-x
OAUTH_AUTHENTIK_CLIENT_ID=x OAUTH_OIDC_CLIENT_ID=x
OAUTH_AUTHENTIK_CLIENT_SECRET=x OAUTH_OIDC_CLIENT_SECRET=x
OAUTH_AUTHENTIK_AUTHORIZE_URL=http://localhost:9000/application/o/authorize/ OAUTH_OIDC_AUTHORIZE_URL=http://localhost:9000/application/o/authorize/
OAUTH_AUTHENTIK_USERINFO_URL=http://localhost:9000/application/o/userinfo/ OAUTH_OIDC_USERINFO_URL=http://localhost:9000/application/o/userinfo/
OAUTH_AUTHENTIK_TOKEN_URL=http://localhost:9000/application/o/token/ OAUTH_OIDC_TOKEN_URL=http://localhost:9000/application/o/token/
FEATURES_OAUTH_REGISTRATION=true FEATURES_OAUTH_REGISTRATION=true
FEATURES_USER_REGISTRATION=true FEATURES_USER_REGISTRATION=true

View file

@ -29,6 +29,7 @@ model User {
totpSecret String? totpSecret String?
passkeys UserPasskey[] passkeys UserPasskey[]
sessions String[]
quota UserQuota? quota UserQuota?
@ -103,7 +104,7 @@ enum OAuthProviderType {
DISCORD DISCORD
GOOGLE GOOGLE
GITHUB GITHUB
AUTHENTIK OIDC
} }
model File { model File {
@ -112,7 +113,7 @@ model File {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletesAt DateTime? deletesAt DateTime?
name String // name & file saved on datasource name String // name & file saved on datasource
originalName String? // original name of file when uploaded originalName String? // original name of file when uploaded
size BigInt size BigInt
type String type String
@ -232,4 +233,4 @@ model Invite {
inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade) inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade)
inviterId String inviterId String
} }

View file

@ -24,14 +24,14 @@ const icons = {
DISCORD: <IconBrandDiscordFilled size='1rem' />, DISCORD: <IconBrandDiscordFilled size='1rem' />,
GITHUB: <IconBrandGithubFilled size='1rem' />, GITHUB: <IconBrandGithubFilled size='1rem' />,
GOOGLE: <IconBrandGoogleFilled size='1rem' stroke={4} />, GOOGLE: <IconBrandGoogleFilled size='1rem' stroke={4} />,
AUTHENTIK: <IconCircleKeyFilled size='1rem' />, OIDC: <IconCircleKeyFilled size='1rem' />,
}; };
const names = { const names = {
DISCORD: 'Discord', DISCORD: 'Discord',
GITHUB: 'GitHub', GITHUB: 'GitHub',
GOOGLE: 'Google', GOOGLE: 'Google',
AUTHENTIK: 'Authentik', OIDC: 'OpenID Connect',
}; };
function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked: boolean }) { function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked: boolean }) {
@ -90,7 +90,7 @@ export default function SettingsOAuth() {
const discordLinked = findProvider('DISCORD', user?.oauthProviders ?? []); const discordLinked = findProvider('DISCORD', user?.oauthProviders ?? []);
const githubLinked = findProvider('GITHUB', user?.oauthProviders ?? []); const githubLinked = findProvider('GITHUB', user?.oauthProviders ?? []);
const googleLinked = findProvider('GOOGLE', user?.oauthProviders ?? []); const googleLinked = findProvider('GOOGLE', user?.oauthProviders ?? []);
const authentikLinked = findProvider('AUTHENTIK', user?.oauthProviders ?? []); const oidcLinked = findProvider('OIDC', user?.oauthProviders ?? []);
return ( return (
<Paper withBorder p='sm'> <Paper withBorder p='sm'>
@ -103,7 +103,7 @@ export default function SettingsOAuth() {
{config.oauthEnabled.discord && <OAuthButton provider='DISCORD' linked={!!discordLinked} />} {config.oauthEnabled.discord && <OAuthButton provider='DISCORD' linked={!!discordLinked} />}
{config.oauthEnabled.github && <OAuthButton provider='GITHUB' linked={!!githubLinked} />} {config.oauthEnabled.github && <OAuthButton provider='GITHUB' linked={!!githubLinked} />}
{config.oauthEnabled.google && <OAuthButton provider='GOOGLE' linked={!!googleLinked} />} {config.oauthEnabled.google && <OAuthButton provider='GOOGLE' linked={!!googleLinked} />}
{config.oauthEnabled.authentik && <OAuthButton provider='AUTHENTIK' linked={!!authentikLinked} />} {config.oauthEnabled.oidc && <OAuthButton provider='OIDC' linked={!!oidcLinked} />}
</Group> </Group>
</Paper> </Paper>
); );

View file

@ -102,7 +102,7 @@ export const rawConfig: any = {
clientId: undefined, clientId: undefined,
clientSecret: undefined, clientSecret: undefined,
}, },
authentik: { oidc: {
clientId: undefined, clientId: undefined,
clientSecret: undefined, clientSecret: undefined,
authorizeUrl: undefined, authorizeUrl: undefined,
@ -211,11 +211,11 @@ export const PROP_TO_ENV = {
'oauth.github.clientSecret': 'OAUTH_GITHUB_CLIENT_SECRET', 'oauth.github.clientSecret': 'OAUTH_GITHUB_CLIENT_SECRET',
'oauth.google.clientId': 'OAUTH_GOOGLE_CLIENT_ID', 'oauth.google.clientId': 'OAUTH_GOOGLE_CLIENT_ID',
'oauth.google.clientSecret': 'OAUTH_GOOGLE_CLIENT_SECRET', 'oauth.google.clientSecret': 'OAUTH_GOOGLE_CLIENT_SECRET',
'oauth.authentik.clientId': 'OAUTH_AUTHENTIK_CLIENT_ID', 'oauth.oidc.clientId': 'OAUTH_OIDC_CLIENT_ID',
'oauth.authentik.clientSecret': 'OAUTH_AUTHENTIK_CLIENT_SECRET', 'oauth.oidc.clientSecret': 'OAUTH_OIDC_CLIENT_SECRET',
'oauth.authentik.authorizeUrl': 'OAUTH_AUTHENTIK_AUTHORIZE_URL', 'oauth.oidc.authorizeUrl': 'OAUTH_OIDC_AUTHORIZE_URL',
'oauth.authentik.userinfoUrl': 'OAUTH_AUTHENTIK_USERINFO_URL', 'oauth.oidc.userinfoUrl': 'OAUTH_OIDC_USERINFO_URL',
'oauth.authentik.tokenUrl': 'OAUTH_AUTHENTIK_TOKEN_URL', 'oauth.oidc.tokenUrl': 'OAUTH_OIDC_TOKEN_URL',
'discord.webhookUrl': 'DISCORD_WEBHOOK_URL', 'discord.webhookUrl': 'DISCORD_WEBHOOK_URL',
'discord.username': 'DISCORD_USERNAME', 'discord.username': 'DISCORD_USERNAME',
@ -339,11 +339,11 @@ export function readEnv() {
env('oauth.github.clientSecret', 'string'), env('oauth.github.clientSecret', 'string'),
env('oauth.google.clientId', 'string'), env('oauth.google.clientId', 'string'),
env('oauth.google.clientSecret', 'string'), env('oauth.google.clientSecret', 'string'),
env('oauth.authentik.clientId', 'string'), env('oauth.oidc.clientId', 'string'),
env('oauth.authentik.clientSecret', 'string'), env('oauth.oidc.clientSecret', 'string'),
env('oauth.authentik.authorizeUrl', 'string'), env('oauth.oidc.authorizeUrl', 'string'),
env('oauth.authentik.userinfoUrl', 'string'), env('oauth.oidc.userinfoUrl', 'string'),
env('oauth.authentik.tokenUrl', 'string'), env('oauth.oidc.tokenUrl', 'string'),
env('discord.webhookUrl', 'string'), env('discord.webhookUrl', 'string'),
env('discord.username', 'string'), env('discord.username', 'string'),

View file

@ -242,7 +242,7 @@ export const schema = z.object({
clientSecret: z.undefined(), clientSecret: z.undefined(),
}), }),
), ),
authentik: z oidc: z
.object({ .object({
clientId: z.string(), clientId: z.string(),
clientSecret: z.string(), clientSecret: z.string(),

View file

@ -9,6 +9,8 @@ export type User = {
role: 'USER' | 'ADMIN' | 'SUPERADMIN'; role: 'USER' | 'ADMIN' | 'SUPERADMIN';
view: UserViewSettings; view: UserViewSettings;
sessions: string[];
oauthProviders: OAuthProvider[]; oauthProviders: OAuthProvider[];
totpSecret?: string | null; totpSecret?: string | null;
@ -32,6 +34,7 @@ export const userSelect = {
totpSecret: true, totpSecret: true,
passkeys: true, passkeys: true,
quota: true, quota: true,
sessions: true,
}; };
export type UserViewSettings = z.infer<typeof userViewSchema>; export type UserViewSettings = z.infer<typeof userViewSchema>;

View file

@ -6,6 +6,7 @@ import { User, userSelect } from '../db/models/user';
import { NextApiReq, NextApiRes } from '../response'; import { NextApiReq, NextApiRes } from '../response';
import { Handler } from './combine'; import { Handler } from './combine';
import { isAdministrator } from '../role'; import { isAdministrator } from '../role';
import { getSession } from '@/server/session';
export type ZiplineAuthOptions = { export type ZiplineAuthOptions = {
administratorOnly?: boolean; administratorOnly?: boolean;
@ -47,28 +48,19 @@ export function parseUserToken(
export function ziplineAuth(options?: ZiplineAuthOptions) { export function ziplineAuth(options?: ZiplineAuthOptions) {
return (handler: Handler) => { return (handler: Handler) => {
return async (req: NextApiReq, res: NextApiRes) => { return async (req: NextApiReq, res: NextApiRes) => {
let rawToken: string | undefined; const session = await getSession(req, res);
if (!session.id || !session.sessionId) return res.unauthorized('invalid session, not logged in');
if (req.cookies.zipline_token) rawToken = req.cookies.zipline_token;
else if (req.headers.authorization) rawToken = req.headers.authorization;
try {
// eslint-disable-next-line no-var
var token = parseUserToken(rawToken);
} catch (e) {
return res.unauthorized((e as { error: string }).error);
}
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
token, sessions: {
}, has: session.sessionId,
select: { },
...userSelect,
...(options?.select && options.select),
}, },
select: userSelect,
}); });
if (!user) return res.unauthorized();
if (!user) return res.unauthorized('invalid login session');
req.user = user; req.user = user;

View file

@ -20,12 +20,12 @@ export default function enabled(config: Config) {
config.features.oauthRegistration, config.features.oauthRegistration,
); );
const authentikEnabled = isTruthy( const oidcEnabled = isTruthy(
config.oauth?.authentik?.clientId, config.oauth?.oidc?.clientId,
config.oauth?.authentik?.clientSecret, config.oauth?.oidc?.clientSecret,
config.oauth?.authentik?.authorizeUrl, config.oauth?.oidc?.authorizeUrl,
config.oauth?.authentik?.tokenUrl, config.oauth?.oidc?.tokenUrl,
config.oauth?.authentik?.userinfoUrl, config.oauth?.oidc?.userinfoUrl,
config.features.oauthRegistration, config.features.oauthRegistration,
); );
@ -33,6 +33,6 @@ export default function enabled(config: Config) {
discord: discordEnabled, discord: discordEnabled,
github: githubEnabled, github: githubEnabled,
google: googleEnabled, google: googleEnabled,
authentik: authentikEnabled, oidc: oidcEnabled,
}; };
} }

View file

@ -61,10 +61,10 @@ export const googleAuth = {
}, },
}; };
export const authentikAuth = { export const oidcAuth = {
url: (clientId: string, origin: string, authorizeUrl: string, state?: string) => url: (clientId: string, origin: string, authorizeUrl: string, state?: string) =>
`${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent( `${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/authentik`, `${origin}/api/auth/oauth/oidc`,
)}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${state}` : ''}`, )}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${state}` : ''}`,
user: async (accessToken: string, userInfoUrl: string) => { user: async (accessToken: string, userInfoUrl: string) => {
const res = await fetch(userInfoUrl, { const res = await fetch(userInfoUrl, {

View file

@ -1,13 +1,12 @@
import { NextApiReq, NextApiRes } from '@/lib/response'; import { NextApiReq, NextApiRes } from '@/lib/response';
import { OAuthProviderType } from '@prisma/client'; import { OAuthProviderType } from '@prisma/client';
import { prisma } from '../db'; import { prisma } from '../db';
import { parseUserToken } from '../middleware/ziplineAuth';
import { findProvider } from './providerUtil'; import { findProvider } from './providerUtil';
import { createToken } from '../crypto'; import { createToken } from '../crypto';
import { config } from '../config'; import { config } from '../config';
import { User } from '../db/models/user'; import { User } from '../db/models/user';
import Logger, { log } from '../logger'; import Logger, { log } from '../logger';
import { getSession } from '@/server/session'; import { getSession, saveSession } from '@/server/session';
export interface OAuthQuery { export interface OAuthQuery {
state?: string; state?: string;
@ -76,15 +75,11 @@ export const withOAuth =
const { state } = req.query as OAuthQuery; const { state } = req.query as OAuthQuery;
let rawToken: string | undefined;
if (req.cookies.zipline_token) rawToken = req.cookies.zipline_token;
else if (req.headers.authorization) rawToken = req.headers.authorization;
const token = parseUserToken(rawToken, true);
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
token: token ?? '', sessions: {
has: session.sessionId ?? '',
},
}, },
include: { include: {
oauthProviders: true, oauthProviders: true,
@ -94,7 +89,7 @@ export const withOAuth =
const userOauth = findProvider(provider, user?.oauthProviders ?? []); const userOauth = findProvider(provider, user?.oauthProviders ?? []);
if (state === 'link') { if (state === 'link') {
if (!user) return res.unauthorized(); if (!user) return res.unauthorized('invalid session');
if (findProvider(provider, user.oauthProviders)) if (findProvider(provider, user.oauthProviders))
return res.badRequest('This account is already linked to this provider'); return res.badRequest('This account is already linked to this provider');
@ -122,8 +117,7 @@ export const withOAuth =
}, },
}); });
session.user = user; await saveSession(session, <User>user);
await session.save();
logger.info('linked oauth account', { logger.info('linked oauth account', {
provider, provider,
@ -153,8 +147,7 @@ export const withOAuth =
}, },
}); });
session.user = user; await saveSession(session, <User>user);
await session.save();
return res.redirect('/dashboard'); return res.redirect('/dashboard');
} else if (existingOauth) { } else if (existingOauth) {
@ -173,8 +166,7 @@ export const withOAuth =
}, },
}); });
session.user = login.user! as User; await saveSession(session, <User>login.user!);
await session.save();
logger.info('logged in with oauth', { logger.info('logged in with oauth', {
provider, provider,
@ -206,8 +198,7 @@ export const withOAuth =
}, },
}); });
session.user = nuser as User; await saveSession(session, <User>nuser);
await session.save();
logger.info('created user with oauth', { logger.info('created user with oauth', {
provider, provider,

View file

@ -69,7 +69,7 @@ export async function handleOverrideColors(theme: ZiplineTheme) {
...theme.colors, ...theme.colors,
google: theme.colors?.google || Array(10).fill('#4285F4'), google: theme.colors?.google || Array(10).fill('#4285F4'),
github: theme.colors?.github || Array(10).fill('#24292E'), github: theme.colors?.github || Array(10).fill('#24292E'),
authentik: theme.colors?.authentik || Array(10).fill('#FD4B2D'), oidc: theme.colors?.oidc || Array(10).fill('#72abcf'),
discord: theme.colors?.discord || Array(10).fill('#5865F2'), discord: theme.colors?.discord || Array(10).fill('#5865F2'),
}, },
} as ZiplineTheme; } as ZiplineTheme;

View file

@ -3,7 +3,7 @@ import Logger from '@/lib/logger';
import { combine } from '@/lib/middleware/combine'; import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method'; import { method } from '@/lib/middleware/method';
import enabled from '@/lib/oauth/enabled'; import enabled from '@/lib/oauth/enabled';
import { authentikAuth } from '@/lib/oauth/providerUtil'; import { oidcAuth } from '@/lib/oauth/providerUtil';
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth'; import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
// thanks to @danejur for this https://github.com/diced/zipline/pull/372 // thanks to @danejur for this https://github.com/diced/zipline/pull/372
@ -14,33 +14,33 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
error_code: 403, error_code: 403,
}; };
const { authentik: authentikEnabled } = enabled(config); const { oidc: oidcEnabled } = enabled(config);
if (!authentikEnabled) if (!oidcEnabled)
return { return {
error: 'Authentik OAuth is not configured.', error: 'OpenID Connect OAuth is not configured.',
error_code: 401, error_code: 401,
}; };
if (!code) if (!code)
return { return {
redirect: authentikAuth.url( redirect: oidcAuth.url(
config.oauth.authentik.clientId!, config.oauth.oidc.clientId!,
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`, `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
config.oauth.authentik.authorizeUrl!, config.oauth.oidc.authorizeUrl!,
state, state,
), ),
}; };
const body = new URLSearchParams({ const body = new URLSearchParams({
client_id: config.oauth.authentik.clientId!, client_id: config.oauth.oidc.clientId!,
client_secret: config.oauth.authentik.clientSecret!, client_secret: config.oauth.oidc.clientSecret!,
grant_type: 'authorization_code', grant_type: 'authorization_code',
code, code,
redirect_uri: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/authentik`, redirect_uri: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/oidc`,
}); });
const res = await fetch(config.oauth.authentik.tokenUrl!, { const res = await fetch(config.oauth.oidc.tokenUrl!, {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@ -57,7 +57,7 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
if (!json.access_token) return { error: 'No access token in response' }; if (!json.access_token) return { error: 'No access token in response' };
if (!json.refresh_token) return { error: 'No refresh token in response' }; if (!json.refresh_token) return { error: 'No refresh token in response' };
const userJson = await authentikAuth.user(json.access_token, config.oauth.authentik.userinfoUrl!); const userJson = await oidcAuth.user(json.access_token, config.oauth.oidc.userinfoUrl!);
if (!userJson) return { error: 'Failed to fetch user' }; if (!userJson) return { error: 'Failed to fetch user' };
return { return {
@ -68,4 +68,4 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
}; };
} }
export default combine([method(['GET'])], withOAuth('AUTHENTIK', handler)); export default combine([method(['GET'])], withOAuth('OIDC', handler));

View file

@ -332,8 +332,8 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
leftSection={<IconBrandGoogleFilled stroke={4} />} leftSection={<IconBrandGoogleFilled stroke={4} />}
/> />
)} )}
{config.oauthEnabled.authentik && ( {config.oauthEnabled.oidc && (
<ExternalAuthButton provider='Authentik' alpha={0.2} leftSection={<IconCircleKeyFilled />} /> <ExternalAuthButton provider='OIDC' alpha={0.2} leftSection={<IconCircleKeyFilled />} />
)} )}
</Stack> </Stack>
</Card> </Card>

View file

@ -41,11 +41,13 @@ export function parseUserToken(
export async function userMiddleware(req: FastifyRequest, res: FastifyReply) { export async function userMiddleware(req: FastifyRequest, res: FastifyReply) {
const session = await getSession(req, res); const session = await getSession(req, res);
if (!session.user) return res.unauthorized('not logged in'); if (!session.id || !session.sessionId) return res.unauthorized('not logged in');
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
password: session.user.password, sessions: {
has: session.sessionId,
},
}, },
select: userSelect, select: userSelect,
}); });

View file

@ -2,7 +2,7 @@ import { verifyPassword } from '@/lib/crypto';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { User, userSelect } from '@/lib/db/models/user'; import { User, userSelect } from '@/lib/db/models/user';
import { verifyTotpCode } from '@/lib/totp'; import { verifyTotpCode } from '@/lib/totp';
import { getSession } from '@/server/session'; import { getSession, saveSession } from '@/server/session';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
export type ApiLoginResponse = { export type ApiLoginResponse = {
@ -27,7 +27,8 @@ export default fastifyPlugin(
handler: async (req, res) => { handler: async (req, res) => {
const session = await getSession(req, res); const session = await getSession(req, res);
session.user = null; session.id = null;
session.sessionId = null;
const { username, password, code } = req.body; const { username, password, code } = req.body;
@ -60,8 +61,7 @@ export default fastifyPlugin(
totp: true, totp: true,
}); });
session.user = user; await saveSession(session, user as User, false);
await session.save();
delete (user as any).password; delete (user as any).password;

View file

@ -1,8 +1,8 @@
import { config } from '@/lib/config'; import { config } from '@/lib/config';
import { createToken, hashPassword } from '@/lib/crypto'; import { createToken, hashPassword } from '@/lib/crypto';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { userSelect } from '@/lib/db/models/user'; import { User, userSelect } from '@/lib/db/models/user';
import { getSession } from '@/server/session'; import { getSession, saveSession } from '@/server/session';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
import { ApiLoginResponse } from './login'; import { ApiLoginResponse } from './login';
@ -76,8 +76,7 @@ export default fastifyPlugin(
}, },
}); });
session.user = user; await saveSession(session, <User>user);
await session.save();
delete (user as any).password; delete (user as any).password;

View file

@ -1,7 +1,7 @@
import { config } from '@/lib/config'; import { config } from '@/lib/config';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { User, userSelect } from '@/lib/db/models/user'; import { User, userSelect } from '@/lib/db/models/user';
import { getSession } from '@/server/session'; import { getSession, saveSession } from '@/server/session';
import { AuthenticationResponseJSON } from '@github/webauthn-json/dist/types/browser-ponyfill'; import { AuthenticationResponseJSON } from '@github/webauthn-json/dist/types/browser-ponyfill';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
@ -47,8 +47,7 @@ export default fastifyPlugin(
}); });
if (!user) return res.badRequest('Invalid passkey'); if (!user) return res.badRequest('Invalid passkey');
session.user = user; await saveSession(session, <User>user);
await session.save();
delete (user as any).password; delete (user as any).password;

View file

@ -2,7 +2,7 @@ import { hashPassword } from '@/lib/crypto';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { User, userSelect } from '@/lib/db/models/user'; import { User, userSelect } from '@/lib/db/models/user';
import { userMiddleware } from '@/server/middleware/user'; import { userMiddleware } from '@/server/middleware/user';
import { getSession } from '@/server/session'; import { getSession, saveSession } from '@/server/session';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
export type ApiUserResponse = { export type ApiUserResponse = {
@ -75,12 +75,13 @@ export default fastifyPlugin(
}, },
select: { select: {
...userSelect, ...userSelect,
password: true,
token: true,
}, },
}); });
const session = await getSession(req, res); const session = await getSession(req, res);
session.user = user; await saveSession(session, <User>user);
await session.save();
delete (user as any).password; delete (user as any).password;

View file

@ -3,7 +3,6 @@ import { createToken, encryptToken } from '@/lib/crypto';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { User, userSelect } from '@/lib/db/models/user'; import { User, userSelect } from '@/lib/db/models/user';
import { userMiddleware } from '@/server/middleware/user'; import { userMiddleware } from '@/server/middleware/user';
import { getSession } from '@/server/session';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
export type ApiUserTokenResponse = { export type ApiUserTokenResponse = {
@ -32,8 +31,6 @@ export default fastifyPlugin(
}); });
server.patch(PATH, { preHandler: [userMiddleware] }, async (req, res) => { server.patch(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
const session = await getSession(req, res);
const user = await prisma.user.update({ const user = await prisma.user.update({
where: { where: {
id: req.user.id, id: req.user.id,
@ -47,8 +44,6 @@ export default fastifyPlugin(
}, },
}); });
session.user!.token = user.token;
delete (user as any).password; delete (user as any).password;
return res.send({ return res.send({

View file

@ -1,5 +1,7 @@
import { config } from '@/lib/config'; import { config } from '@/lib/config';
import { prisma } from '@/lib/db';
import { User } from '@/lib/db/models/user'; import { User } from '@/lib/db/models/user';
import { randomCharacters } from '@/lib/random';
import { FastifyReply, FastifyRequest } from 'fastify'; import { FastifyReply, FastifyRequest } from 'fastify';
import { IncomingMessage, ServerResponse } from 'http'; import { IncomingMessage, ServerResponse } from 'http';
import { getIronSession } from 'iron-session'; import { getIronSession } from 'iron-session';
@ -12,12 +14,17 @@ const cookieOptions = {
sameSite: 'lax', sameSite: 'lax',
}; };
export type ZiplineSession = {
id: string | null;
sessionId: string | null;
};
export async function getSession( export async function getSession(
req: FastifyRequest | IncomingMessage, req: FastifyRequest | IncomingMessage,
reply: FastifyReply | ServerResponse<IncomingMessage>, reply: FastifyReply | ServerResponse<IncomingMessage>,
) { ) {
if (!(req as any).raw || !(req as any).raw) { if (!(req as any).raw || !(req as any).raw) {
const session = await getIronSession<{ user: User | null }>( const session = await getIronSession<ZiplineSession>(
req as IncomingMessage, req as IncomingMessage,
reply as ServerResponse<IncomingMessage>, reply as ServerResponse<IncomingMessage>,
{ {
@ -30,7 +37,7 @@ export async function getSession(
return session; return session;
} }
const session = await getIronSession<{ user: User | null }>( const session = await getIronSession<ZiplineSession>(
(req as FastifyRequest).raw, (req as FastifyRequest).raw,
(reply as FastifyReply).raw, (reply as FastifyReply).raw,
{ {
@ -42,3 +49,25 @@ export async function getSession(
return session; return session;
} }
export async function saveSession(
session: Awaited<ReturnType<typeof getSession>>,
user: User,
overwriteSessions = true,
) {
session.id = user.id;
const sessionId = randomCharacters(32);
session.sessionId = sessionId;
await prisma.user.update({
where: {
id: user.id,
},
data: {
sessions: overwriteSessions ? { set: [sessionId] } : { push: sessionId },
},
});
await session.save();
}