mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: sessions
This commit is contained in:
@@ -56,6 +56,7 @@
|
||||
"fflate": "^0.8.2",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"highlight.js": "^11.9.0",
|
||||
"iron-session": "^8.0.3",
|
||||
"isomorphic-dompurify": "^1.11.0",
|
||||
"katex": "^0.16.9",
|
||||
"mantine-datatable": "^7.1.7",
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -116,6 +116,9 @@ importers:
|
||||
highlight.js:
|
||||
specifier: ^11.9.0
|
||||
version: 11.9.0
|
||||
iron-session:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
isomorphic-dompurify:
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0
|
||||
@@ -3100,6 +3103,12 @@ packages:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
iron-session@8.0.3:
|
||||
resolution: {integrity: sha512-WtDX0griBliMoR6hGoU3SlefW+VSbfHrIVqURQ0Nbg/Pd+nj7VDsKV+sx0FHjyUCaO02YoYV5v+kW0PqvFJISQ==}
|
||||
|
||||
iron-webcrypto@1.2.1:
|
||||
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
|
||||
|
||||
is-arguments@1.1.1:
|
||||
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5120,6 +5129,9 @@ packages:
|
||||
unbox-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
|
||||
uncrypto@0.1.3:
|
||||
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
@@ -9235,6 +9247,14 @@ snapshots:
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
iron-session@8.0.3:
|
||||
dependencies:
|
||||
cookie: 0.6.0
|
||||
iron-webcrypto: 1.2.1
|
||||
uncrypto: 0.1.3
|
||||
|
||||
iron-webcrypto@1.2.1: {}
|
||||
|
||||
is-arguments@1.1.1:
|
||||
dependencies:
|
||||
call-bind: 1.0.5
|
||||
@@ -11510,6 +11530,8 @@ snapshots:
|
||||
has-symbols: 1.0.3
|
||||
which-boxed-primitive: 1.0.2
|
||||
|
||||
uncrypto@0.1.3: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
unified@10.1.2:
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function Layout({ children, config }: { children: React.ReactNode
|
||||
const router = useRouter();
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const [setUser, setToken] = useUserStore((s) => [s.setUser, s.setToken]);
|
||||
const [setUser] = useUserStore((s) => [s.setUser]);
|
||||
|
||||
const { user, mutate } = useLogin();
|
||||
const { avatar } = useAvatar();
|
||||
@@ -163,7 +163,7 @@ export default function Layout({ children, config }: { children: React.ReactNode
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
});
|
||||
} else {
|
||||
clipboard.copy(data?.token);
|
||||
clipboard.copy(data?.token ?? '');
|
||||
showNotification({
|
||||
title: 'Copied',
|
||||
message: 'Your token has been copied to your clipboard.',
|
||||
@@ -196,7 +196,6 @@ export default function Layout({ children, config }: { children: React.ReactNode
|
||||
icon: <IconRefreshDot size='1rem' />,
|
||||
});
|
||||
} else {
|
||||
setToken(data?.token);
|
||||
setUser(data?.user);
|
||||
mutate(data as Response['/api/user']);
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ import { isAdministrator } from '../role';
|
||||
|
||||
export default function useLogin(administratorOnly: boolean = false) {
|
||||
const router = useRouter();
|
||||
const { data, error, isLoading, mutate } = useSWR<Response['/api/user']>('/api/user');
|
||||
const { data, error, isLoading, mutate } = useSWR<Response['/api/user']>('/api/user', {
|
||||
fallbackData: { user: undefined },
|
||||
});
|
||||
|
||||
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
|
||||
const [token, setToken] = useUserStore((state) => [state.token, state.setToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.user && data?.token) {
|
||||
if (data?.user) {
|
||||
setUser(data.user);
|
||||
setToken(data.token);
|
||||
} else if (error) {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
@@ -27,5 +27,5 @@ export default function useLogin(administratorOnly: boolean = false) {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return { user, token, loading: isLoading || !user, mutate };
|
||||
return { user, loading: isLoading || !user, mutate };
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { config } from './config';
|
||||
import { serializeCookie } from './cookie';
|
||||
import { encryptToken } from './crypto';
|
||||
import { User } from './db/models/user';
|
||||
import { NextApiRes } from './response';
|
||||
|
||||
export function loginToken(res: NextApiRes, user: User) {
|
||||
const token = encryptToken(user.token!, config.core.secret);
|
||||
|
||||
const cookie = serializeCookie('zipline_token', token, {
|
||||
// week
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000),
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
res.setHeader('Set-Cookie', cookie);
|
||||
|
||||
return token;
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import { parseUserToken } from '../middleware/ziplineAuth';
|
||||
import { findProvider } from './providerUtil';
|
||||
import { createToken } from '../crypto';
|
||||
import { config } from '../config';
|
||||
import { loginToken } from '../login';
|
||||
import { User } from '../db/models/user';
|
||||
import Logger, { log } from '../logger';
|
||||
import { getSession } from '@/server/session';
|
||||
|
||||
export interface OAuthQuery {
|
||||
state?: string;
|
||||
@@ -39,6 +39,8 @@ export const withOAuth =
|
||||
|
||||
const response = await oauthProfile(req.query as OAuthQuery, logger);
|
||||
|
||||
const session = await getSession(req, res);
|
||||
|
||||
if (response.error) {
|
||||
return res.serverError(response.error, {
|
||||
oauth: response.error_code,
|
||||
@@ -120,7 +122,8 @@ export const withOAuth =
|
||||
},
|
||||
});
|
||||
|
||||
loginToken(res, user);
|
||||
session.user = user;
|
||||
await session.save();
|
||||
|
||||
logger.info('linked oauth account', {
|
||||
provider,
|
||||
@@ -150,7 +153,8 @@ export const withOAuth =
|
||||
},
|
||||
});
|
||||
|
||||
loginToken(res, user);
|
||||
session.user = user;
|
||||
await session.save();
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
} else if (existingOauth) {
|
||||
@@ -169,7 +173,8 @@ export const withOAuth =
|
||||
},
|
||||
});
|
||||
|
||||
loginToken(res, login.user! as User);
|
||||
session.user = login.user! as User;
|
||||
await session.save();
|
||||
|
||||
logger.info('logged in with oauth', {
|
||||
provider,
|
||||
@@ -201,7 +206,8 @@ export const withOAuth =
|
||||
},
|
||||
});
|
||||
|
||||
loginToken(res, nuser as User);
|
||||
session.user = nuser as User;
|
||||
await session.save();
|
||||
|
||||
logger.info('created user with oauth', {
|
||||
provider,
|
||||
|
||||
@@ -3,14 +3,10 @@ import type { User } from '../db/models/user';
|
||||
|
||||
type UserStore = {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
setUser: (user?: User | null) => void;
|
||||
setToken: (token?: string | null) => void;
|
||||
};
|
||||
|
||||
export const useUserStore = create<UserStore>()((set) => ({
|
||||
user: null,
|
||||
token: null,
|
||||
setUser: (user) => set({ user }),
|
||||
setToken: (token) => set({ token }),
|
||||
}));
|
||||
|
||||
@@ -6,7 +6,7 @@ import { mutate } from 'swr';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [setUser, setToken] = useUserStore((state) => [state.setUser, state.setToken]);
|
||||
const [setUser] = useUserStore((state) => [state.setUser]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -16,7 +16,6 @@ export default function Login() {
|
||||
|
||||
if (res.ok) {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
mutate('/api/user', null);
|
||||
await router.push('/auth/login');
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { serializeCookie } from '@/lib/cookie';
|
||||
import { encryptToken } from '@/lib/crypto';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
export function loginToken(res: FastifyReply, user: User) {
|
||||
const token = encryptToken(user.token!, config.core.secret);
|
||||
|
||||
const cookie = serializeCookie('zipline_token', token, {
|
||||
// week
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000),
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
res.header('Set-Cookie', cookie);
|
||||
|
||||
return token;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { FastifyRequest } from 'fastify/types/request';
|
||||
import { getSession } from '../session';
|
||||
|
||||
declare module 'fastify' {
|
||||
export interface FastifyRequest {
|
||||
@@ -38,25 +39,17 @@ export function parseUserToken(
|
||||
}
|
||||
|
||||
export async function userMiddleware(req: FastifyRequest, res: FastifyReply) {
|
||||
let rawToken: string | undefined;
|
||||
const session = await getSession(req, res);
|
||||
|
||||
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);
|
||||
}
|
||||
if (!session.user) return res.unauthorized('not logged in');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
token,
|
||||
password: session.user.password,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
if (!user) return res.unauthorized('invalid token');
|
||||
if (!user) return res.unauthorized('invalid login session');
|
||||
|
||||
req.user = user;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { verifyTotpCode } from '@/lib/totp';
|
||||
import { loginToken } from '@/server/loginToken';
|
||||
import { getSession } from '@/server/session';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiLoginResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
totp?: true;
|
||||
};
|
||||
|
||||
@@ -26,6 +25,10 @@ export default fastifyPlugin(
|
||||
url: PATH,
|
||||
method: ['POST'],
|
||||
handler: async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
|
||||
session.user = null;
|
||||
|
||||
const { username, password, code } = req.body;
|
||||
|
||||
if (!username) return res.badRequest('Username is required');
|
||||
@@ -57,13 +60,12 @@ export default fastifyPlugin(
|
||||
totp: true,
|
||||
});
|
||||
|
||||
const token = loginToken(res, user);
|
||||
session.user = user;
|
||||
await session.save();
|
||||
|
||||
delete (user as any).token;
|
||||
delete (user as any).password;
|
||||
|
||||
return res.send({
|
||||
token,
|
||||
user,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import { getSession } from '@/server/session';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiLogoutResponse = {
|
||||
@@ -14,8 +15,9 @@ export default fastifyPlugin(
|
||||
url: PATH,
|
||||
method: ['GET'],
|
||||
preHandler: [userMiddleware],
|
||||
handler: async (_, res) => {
|
||||
res.header('Set-Cookie', 'zipline_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT');
|
||||
handler: async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
session.destroy();
|
||||
|
||||
return res.send({ loggedOut: true });
|
||||
},
|
||||
|
||||
@@ -2,9 +2,9 @@ import { config } from '@/lib/config';
|
||||
import { createToken, hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { userSelect } from '@/lib/db/models/user';
|
||||
import { getSession } from '@/server/session';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { ApiLoginResponse } from './login';
|
||||
import { loginToken } from '@/server/loginToken';
|
||||
|
||||
export type ApiAuthRegisterResponse = ApiLoginResponse;
|
||||
|
||||
@@ -22,6 +22,8 @@ export default fastifyPlugin(
|
||||
url: PATH,
|
||||
method: ['POST'],
|
||||
handler: async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
|
||||
const { username, password, code } = req.body;
|
||||
|
||||
if (code && !config.invites.enabled) return res.badRequest("Invites aren't enabled");
|
||||
@@ -74,13 +76,12 @@ export default fastifyPlugin(
|
||||
},
|
||||
});
|
||||
|
||||
const token = loginToken(res, user);
|
||||
session.user = user;
|
||||
await session.save();
|
||||
|
||||
delete (user as any).token;
|
||||
delete (user as any).password;
|
||||
|
||||
return res.send({
|
||||
token,
|
||||
user,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { loginToken } from '@/server/loginToken';
|
||||
import { getSession } from '@/server/session';
|
||||
import { AuthenticationResponseJSON } from '@github/webauthn-json/dist/types/browser-ponyfill';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiAuthWebauthnResponse = {
|
||||
user: User;
|
||||
token: string;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
@@ -23,6 +22,7 @@ export default fastifyPlugin(
|
||||
url: PATH,
|
||||
method: ['POST'],
|
||||
handler: async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
if (!config.mfa.passkeys) return res.badRequest('Passkeys are not enabled');
|
||||
|
||||
const { auth } = req.body;
|
||||
@@ -47,9 +47,9 @@ export default fastifyPlugin(
|
||||
});
|
||||
if (!user) return res.badRequest('Invalid passkey');
|
||||
|
||||
const token = loginToken(res, user);
|
||||
session.user = user;
|
||||
await session.save();
|
||||
|
||||
delete (user as any).token;
|
||||
delete (user as any).password;
|
||||
|
||||
await prisma.userPasskey.updateMany({
|
||||
@@ -65,7 +65,6 @@ export default fastifyPlugin(
|
||||
});
|
||||
|
||||
return res.send({
|
||||
token,
|
||||
user,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,11 +2,11 @@ import { hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import { getSession } from '@/server/session';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserResponse = {
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type Body = {
|
||||
@@ -78,6 +78,12 @@ export default fastifyPlugin(
|
||||
},
|
||||
});
|
||||
|
||||
const session = await getSession(req, res);
|
||||
session.user = user;
|
||||
await session.save();
|
||||
|
||||
delete (user as any).password;
|
||||
|
||||
return res.send({ user, token: req.cookies.zipline_token });
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { config } from '@/lib/config';
|
||||
import { createToken, encryptToken } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { loginToken } from '@/server/loginToken';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import { getSession } from '@/server/session';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
export type ApiUserTokenResponse = {
|
||||
@@ -32,6 +32,8 @@ export default fastifyPlugin(
|
||||
});
|
||||
|
||||
server.patch(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
@@ -45,13 +47,13 @@ export default fastifyPlugin(
|
||||
},
|
||||
});
|
||||
|
||||
const token = loginToken(res, user);
|
||||
session.user!.token = user.token;
|
||||
|
||||
delete (user as any).token;
|
||||
delete (user as any).password;
|
||||
|
||||
return res.send({
|
||||
user,
|
||||
token,
|
||||
token: encryptToken(user.token, config.core.secret),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
44
src/server/session.ts
Normal file
44
src/server/session.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
import { getIronSession } from 'iron-session';
|
||||
|
||||
const cookieOptions = {
|
||||
// week
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000),
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
};
|
||||
|
||||
export async function getSession(
|
||||
req: FastifyRequest | IncomingMessage,
|
||||
reply: FastifyReply | ServerResponse<IncomingMessage>,
|
||||
) {
|
||||
if (!(req as any).raw || !(req as any).raw) {
|
||||
const session = await getIronSession<{ user: User | null }>(
|
||||
req as IncomingMessage,
|
||||
reply as ServerResponse<IncomingMessage>,
|
||||
{
|
||||
password: config.core.secret,
|
||||
cookieName: 'zipline_session',
|
||||
cookieOptions,
|
||||
},
|
||||
);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
const session = await getIronSession<{ user: User | null }>(
|
||||
(req as FastifyRequest).raw,
|
||||
(reply as FastifyReply).raw,
|
||||
{
|
||||
password: config.core.secret,
|
||||
cookieName: 'zipline_session',
|
||||
cookieOptions,
|
||||
},
|
||||
);
|
||||
|
||||
return session;
|
||||
}
|
||||
Reference in New Issue
Block a user