mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
updated auth routes to use sessions and "httpOnly" cookies
This commit is contained in:
@@ -1,10 +1,25 @@
|
|||||||
import { withAuth, withCors } from "./auth/middleware";
|
import { withAuth, withCors, withCSRF } from "./auth/middleware";
|
||||||
import { testDB } from "./db/client";
|
import { testDB } from "./db/client";
|
||||||
|
import { cleanupExpiredSessions } from "./db/queries";
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null;
|
const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null;
|
||||||
const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0;
|
const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0;
|
||||||
|
|
||||||
|
const SESSION_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour in ms
|
||||||
|
|
||||||
|
const startSessionCleanup = () => {
|
||||||
|
const cleanup = async () => {
|
||||||
|
const count = await cleanupExpiredSessions();
|
||||||
|
if (count > 0) {
|
||||||
|
console.log(`cleaned up ${count} expired sessions`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
setInterval(cleanup, SESSION_CLEANUP_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: Number(PORT),
|
port: Number(PORT),
|
||||||
@@ -12,35 +27,39 @@ const main = async () => {
|
|||||||
"/": withCors(() => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`)),
|
"/": withCors(() => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`)),
|
||||||
"/health": withCors(() => new Response("OK")),
|
"/health": withCors(() => new Response("OK")),
|
||||||
|
|
||||||
|
// routes that modify state require withCSRF middleware
|
||||||
"/auth/register": withCors(routes.authRegister),
|
"/auth/register": withCors(routes.authRegister),
|
||||||
"/auth/login": withCors(routes.authLogin),
|
"/auth/login": withCors(routes.authLogin),
|
||||||
|
"/auth/logout": withCors(withAuth(withCSRF(routes.authLogout))),
|
||||||
"/auth/me": withCors(withAuth(routes.authMe)),
|
"/auth/me": withCors(withAuth(routes.authMe)),
|
||||||
|
|
||||||
"/user/by-username": withCors(withAuth(routes.userByUsername)),
|
"/user/by-username": withCors(withAuth(routes.userByUsername)),
|
||||||
"/user/update": withCors(withAuth(routes.userUpdate)),
|
"/user/update": withCors(withAuth(withCSRF(routes.userUpdate))),
|
||||||
"/user/upload-avatar": withCors(routes.userUploadAvatar),
|
"/user/upload-avatar": withCors(withAuth(withCSRF(routes.userUploadAvatar))),
|
||||||
|
|
||||||
"/issue/create": withCors(withAuth(routes.issueCreate)),
|
"/issue/create": withCors(withAuth(withCSRF(routes.issueCreate))),
|
||||||
"/issue/update": withCors(withAuth(routes.issueUpdate)),
|
"/issue/update": withCors(withAuth(withCSRF(routes.issueUpdate))),
|
||||||
"/issue/delete": withCors(withAuth(routes.issueDelete)),
|
"/issue/delete": withCors(withAuth(withCSRF(routes.issueDelete))),
|
||||||
|
|
||||||
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
|
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
|
||||||
"/issues/all": withCors(withAuth(routes.issues)),
|
"/issues/all": withCors(withAuth(routes.issues)),
|
||||||
|
|
||||||
"/organisation/create": withCors(withAuth(routes.organisationCreate)),
|
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
||||||
"/organisation/by-id": withCors(withAuth(routes.organisationById)),
|
"/organisation/by-id": withCors(withAuth(routes.organisationById)),
|
||||||
"/organisation/update": withCors(withAuth(routes.organisationUpdate)),
|
"/organisation/update": withCors(withAuth(withCSRF(routes.organisationUpdate))),
|
||||||
"/organisation/delete": withCors(withAuth(routes.organisationDelete)),
|
"/organisation/delete": withCors(withAuth(withCSRF(routes.organisationDelete))),
|
||||||
"/organisation/add-member": withCors(withAuth(routes.organisationAddMember)),
|
"/organisation/add-member": withCors(withAuth(withCSRF(routes.organisationAddMember))),
|
||||||
"/organisation/members": withCors(withAuth(routes.organisationMembers)),
|
"/organisation/members": withCors(withAuth(routes.organisationMembers)),
|
||||||
"/organisation/remove-member": withCors(withAuth(routes.organisationRemoveMember)),
|
"/organisation/remove-member": withCors(withAuth(withCSRF(routes.organisationRemoveMember))),
|
||||||
"/organisation/update-member-role": withCors(withAuth(routes.organisationUpdateMemberRole)),
|
"/organisation/update-member-role": withCors(
|
||||||
|
withAuth(withCSRF(routes.organisationUpdateMemberRole)),
|
||||||
|
),
|
||||||
|
|
||||||
"/organisations/by-user": withCors(withAuth(routes.organisationsByUser)),
|
"/organisations/by-user": withCors(withAuth(routes.organisationsByUser)),
|
||||||
|
|
||||||
"/project/create": withCors(withAuth(routes.projectCreate)),
|
"/project/create": withCors(withAuth(withCSRF(routes.projectCreate))),
|
||||||
"/project/update": withCors(withAuth(routes.projectUpdate)),
|
"/project/update": withCors(withAuth(withCSRF(routes.projectUpdate))),
|
||||||
"/project/delete": withCors(withAuth(routes.projectDelete)),
|
"/project/delete": withCors(withAuth(withCSRF(routes.projectDelete))),
|
||||||
"/project/with-creator": withCors(withAuth(routes.projectWithCreator)),
|
"/project/with-creator": withCors(withAuth(routes.projectWithCreator)),
|
||||||
|
|
||||||
"/projects/by-creator": withCors(withAuth(routes.projectsByCreator)),
|
"/projects/by-creator": withCors(withAuth(routes.projectsByCreator)),
|
||||||
@@ -52,6 +71,7 @@ const main = async () => {
|
|||||||
|
|
||||||
console.log(`eussi (issue server) listening on ${server.url}`);
|
console.log(`eussi (issue server) listening on ${server.url}`);
|
||||||
await testDB();
|
await testDB();
|
||||||
|
startSessionCleanup();
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BunRequest } from "bun";
|
import type { BunRequest } from "bun";
|
||||||
import { generateToken, verifyPassword } from "../../auth/utils";
|
import { buildAuthCookie, generateToken, verifyPassword } from "../../auth/utils";
|
||||||
import { getUserByUsername } from "../../db/queries";
|
import { createSession, getUserByUsername } from "../../db/queries";
|
||||||
|
|
||||||
const isNonEmptyString = (value: unknown): value is string =>
|
const isNonEmptyString = (value: unknown): value is string =>
|
||||||
typeof value === "string" && value.trim().length > 0;
|
typeof value === "string" && value.trim().length > 0;
|
||||||
@@ -37,10 +37,24 @@ export default async function login(req: BunRequest) {
|
|||||||
return new Response("invalid credentials", { status: 401 });
|
return new Response("invalid credentials", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = generateToken(user.id);
|
const session = await createSession(user.id);
|
||||||
|
if (!session) {
|
||||||
|
return new Response("failed to create session", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json({
|
const token = generateToken(session.id, user.id);
|
||||||
user: { id: user.id, name: user.name, username: user.username },
|
|
||||||
token,
|
return new Response(
|
||||||
});
|
JSON.stringify({
|
||||||
|
user: { id: user.id, name: user.name, username: user.username, avatarURL: user.avatarURL },
|
||||||
|
csrfToken: session.csrfToken,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Set-Cookie": buildAuthCookie(token),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
19
packages/backend/src/routes/auth/logout.ts
Normal file
19
packages/backend/src/routes/auth/logout.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { AuthedRequest } from "../../auth/middleware";
|
||||||
|
import { buildClearAuthCookie } from "../../auth/utils";
|
||||||
|
import { deleteSession } from "../../db/queries";
|
||||||
|
|
||||||
|
export default async function logout(req: AuthedRequest) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return new Response("method not allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteSession(req.sessionId);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Set-Cookie": buildClearAuthCookie(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,5 +8,10 @@ export default async function me(req: AuthedRequest) {
|
|||||||
return new Response("user not found", { status: 404 });
|
return new Response("user not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json(user as UserRecord);
|
const { passwordHash: _, ...safeUser } = user;
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
user: safeUser as Omit<UserRecord, "passwordHash">,
|
||||||
|
csrfToken: req.csrfToken,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BunRequest } from "bun";
|
import type { BunRequest } from "bun";
|
||||||
import { generateToken, hashPassword } from "../../auth/utils";
|
import { buildAuthCookie, generateToken, hashPassword } from "../../auth/utils";
|
||||||
import { createUser, getUserByUsername } from "../../db/queries";
|
import { createSession, createUser, getUserByUsername } from "../../db/queries";
|
||||||
|
|
||||||
const isNonEmptyString = (value: unknown): value is string =>
|
const isNonEmptyString = (value: unknown): value is string =>
|
||||||
typeof value === "string" && value.trim().length > 0;
|
typeof value === "string" && value.trim().length > 0;
|
||||||
@@ -54,10 +54,24 @@ export default async function register(req: BunRequest) {
|
|||||||
return new Response("failed to create user", { status: 500 });
|
return new Response("failed to create user", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = generateToken(user.id);
|
const session = await createSession(user.id);
|
||||||
|
if (!session) {
|
||||||
|
return new Response("failed to create session", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json({
|
const token = generateToken(session.id, user.id);
|
||||||
token,
|
|
||||||
user,
|
return new Response(
|
||||||
});
|
JSON.stringify({
|
||||||
|
user: { id: user.id, name: user.name, username: user.username, avatarURL: user.avatarURL },
|
||||||
|
csrfToken: session.csrfToken,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Set-Cookie": buildAuthCookie(token),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import authLogin from "./auth/login";
|
import authLogin from "./auth/login";
|
||||||
|
import authLogout from "./auth/logout";
|
||||||
import authMe from "./auth/me";
|
import authMe from "./auth/me";
|
||||||
import authRegister from "./auth/register";
|
import authRegister from "./auth/register";
|
||||||
import issueCreate from "./issue/create";
|
import issueCreate from "./issue/create";
|
||||||
@@ -30,6 +31,7 @@ import userUploadAvatar from "./user/upload-avatar";
|
|||||||
export const routes = {
|
export const routes = {
|
||||||
authRegister,
|
authRegister,
|
||||||
authLogin,
|
authLogin,
|
||||||
|
authLogout,
|
||||||
authMe,
|
authMe,
|
||||||
|
|
||||||
userByUsername,
|
userByUsername,
|
||||||
|
|||||||
Reference in New Issue
Block a user