Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dashboard/app/api/v1/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { apiError } from "@/server/api-error";
export const GET = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand All @@ -33,7 +33,7 @@ export const GET = withCors(async (req) => {
export const POST = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/events/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { apiError } from "@/server/api-error";
export const GET = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/music/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const GET = withCors(async (req: NextRequest) => {
if (cookie) {
userId = cookie.id;
} else {
userId = requireUser(req).id;
userId = (await requireUser(req)).id;
}
} catch (err) {
if (err instanceof UnauthorizedError) {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/reports/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { apiError } from "@/server/api-error";
export const POST = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/sessions/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function PATCH(req: NextRequest, ctx: RouteContext): Promise<NextResponse
return withCors(async (r) => {
let user;
try {
user = requireUser(r);
user = await requireUser(r);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { apiError } from "@/server/api-error";
export const POST = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/sessions/top/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { apiError } from "@/server/api-error";
export const GET = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/tracks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function PATCH(req: NextRequest, ctx: RouteContext): Promise<NextResponse
return withCors(async (r) => {
let user;
try {
user = requireUser(r);
user = await requireUser(r);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/api/v1/tracks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { apiError } from "@/server/api-error";
export const POST = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
4 changes: 2 additions & 2 deletions dashboard/app/api/v1/user/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { apiError } from "@/server/api-error";
export const GET = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand All @@ -22,7 +22,7 @@ export const GET = withCors(async (req) => {
export const PUT = withCors(async (req) => {
let user;
try {
user = requireUser(req);
user = await requireUser(req);
} catch (err) {
if (err instanceof UnauthorizedError) {
return apiError("UNAUTHORIZED", err.message, 401);
Expand Down
5 changes: 4 additions & 1 deletion dashboard/app/dashboard/logout-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";

export function LogoutButton() {
const router = useRouter();
const router = useRouter();
const queryClient = useQueryClient();
const [pending, setPending] = useState(false);

return (
Expand All @@ -16,6 +18,7 @@ export function LogoutButton() {
try {
await fetch("/api/auth/logout", { method: "POST" });
} finally {
queryClient.clear();
router.replace("/login");
router.refresh();
}
Expand Down
89 changes: 0 additions & 89 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"framer-motion": "^12.38.0",
"google-auth-library": "^10.6.2",
"jose": "^6.2.3",
"jsonwebtoken": "^9.0.3",
"next": "16.2.6",
"pg": "^8.20.0",
"prisma": "^7.8.0",
Expand All @@ -31,7 +30,6 @@
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/d3": "^7.4.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19",
Expand Down
17 changes: 9 additions & 8 deletions dashboard/server/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OAuth2Client } from "google-auth-library";
import jwt from "jsonwebtoken";
import { SignJWT } from "jose";
import { prisma } from "@/server/db";

function requireEnv(name: string): string {
Expand Down Expand Up @@ -41,13 +41,14 @@ async function upsertUser(data: {
});
}

function issueJWT(userId: string, email: string): string {
const expiresIn = (process.env.JWT_EXPIRES_IN ?? "7d") as jwt.SignOptions["expiresIn"];
return jwt.sign(
{ sub: userId, email },
requireEnv("JWT_SECRET"),
{ expiresIn },
);
async function issueJWT(userId: string, email: string): Promise<string> {
const secret = requireEnv("JWT_SECRET");
const expiresIn = process.env["JWT_EXPIRES_IN"] ?? "7d";
return new SignJWT({ sub: userId, email })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(expiresIn)
.sign(new TextEncoder().encode(secret));
}

export async function authenticateGoogleUser(googleIdToken: string): Promise<string> {
Expand Down
27 changes: 9 additions & 18 deletions dashboard/server/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { NextRequest } from "next/server";
import jwt from "jsonwebtoken";
import { jwtVerify } from "jose";
import { SESSION_COOKIE } from "@/server/cookies";

Expand All @@ -21,13 +20,16 @@ function isPayload(v: unknown): v is JwtPayload {
return typeof r["sub"] === "string" && typeof r["email"] === "string";
}

export async function verifyJwt(token: string): Promise<JwtPayload> {
const secret = process.env.JWT_SECRET;
function getSecretKey(): Uint8Array {
const secret = process.env["JWT_SECRET"];
if (!secret) throw new Error("JWT_SECRET is not set");
return new TextEncoder().encode(secret);
}

export async function verifyJwt(token: string): Promise<JwtPayload> {
let payload: unknown;
try {
const result = await jwtVerify(token, new TextEncoder().encode(secret));
const result = await jwtVerify(token, getSecretKey());
payload = result.payload;
} catch {
throw new UnauthorizedError("Invalid or expired token");
Expand All @@ -45,20 +47,9 @@ function extractToken(req: NextRequest): string | null {
return req.cookies.get(SESSION_COOKIE)?.value ?? null;
}

export function requireUser(req: NextRequest): { id: string; email: string } {
export async function requireUser(req: NextRequest): Promise<{ id: string; email: string }> {
const token = extractToken(req);
if (!token) throw new UnauthorizedError("Missing authentication credentials");

const secret = process.env.JWT_SECRET;
if (!secret) throw new Error("JWT_SECRET is not set");

let decoded: unknown;
try {
decoded = jwt.verify(token, secret);
} catch {
throw new UnauthorizedError("Invalid or expired token");
}
if (!isPayload(decoded)) throw new UnauthorizedError("Invalid token payload");

return { id: decoded.sub, email: decoded.email };
const payload = await verifyJwt(token);
return { id: payload.sub, email: payload.email };
}
Loading
Loading