diff --git a/package.json b/package.json index b781e1b..102aaee 100755 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@t3-oss/env-nextjs": "^0.10.1", "drizzle-orm": "^0.33.0", "geist": "^1.3.0", + "jsonwebtoken": "^9.0.2", "next": "^14.2.4", "postgres": "^3.4.4", "react": "^18.3.1", @@ -26,6 +27,7 @@ }, "devDependencies": { "@types/eslint": "^8.56.10", + "@types/jsonwebtoken": "^9.0.7", "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea3bd31..e3d9d5f 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: geist: specifier: ^1.3.0 version: 1.3.1(next@14.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 next: specifier: ^14.2.4 version: 14.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -36,6 +39,9 @@ importers: '@types/eslint': specifier: ^8.56.10 version: 8.56.12 + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.7 '@types/node': specifier: ^20.14.10 version: 20.16.5 @@ -539,6 +545,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/node@20.16.5': resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} @@ -785,6 +794,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1005,6 +1017,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1503,10 +1518,20 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1536,9 +1561,30 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1893,6 +1939,9 @@ packages: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} @@ -2450,6 +2499,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 20.16.5 + '@types/node@20.16.5': dependencies: undici-types: 6.19.8 @@ -2768,6 +2821,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} busboy@1.6.0: @@ -2923,6 +2978,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3130,7 +3189,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 @@ -3143,7 +3202,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.11.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -3647,6 +3706,19 @@ snapshots: dependencies: minimist: 1.2.8 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -3654,6 +3726,17 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3679,8 +3762,22 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3979,6 +4076,8 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-regex-test@1.0.3: dependencies: call-bind: 1.0.7 diff --git a/src/app/api/countdown/createOrUpdateCountdown/route.ts b/src/app/api/countdown/createOrUpdateCountdown/route.ts new file mode 100644 index 0000000..6faf5a9 --- /dev/null +++ b/src/app/api/countdown/createOrUpdateCountdown/route.ts @@ -0,0 +1,22 @@ +"use server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { createOrUpdateCountdown } from "~/server/functions"; +import { middleware } from "~/middleware"; + +export const POST = async (request: NextRequest) => { + const middlewareResponse = middleware(request); + if (middlewareResponse) return middlewareResponse; + try { + const { relationshipId, title, date } = await request.json() as { + relationshipId: string; + title: string; + date: string; + }; + await createOrUpdateCountdown(Number.parseInt(relationshipId), title, new Date(date)); + return NextResponse.json({ message: "Countdown created or updated successfully" }); + } catch (error) { + console.error(error); + return NextResponse.json({ message: "Error" }, { status: 500 }); + } +}; diff --git a/src/app/api/countdown/getCountdownByRelationship/route.ts b/src/app/api/countdown/getCountdownByRelationship/route.ts new file mode 100644 index 0000000..18aa55d --- /dev/null +++ b/src/app/api/countdown/getCountdownByRelationship/route.ts @@ -0,0 +1,23 @@ +"use server"; +import { NextResponse } from "next/server"; +import { getCountdownByRelationship } from "~/server/functions"; + +export const GET = async (request: Request) => { + try { + const url = new URL(request.url); + const apiKey = url.searchParams.get("apiKey"); + if (apiKey !== process.env.API_KEY) { + console.log("Invalid API Key"); + return NextResponse.json({ message: "Invalid API Key" }, { status: 401 }); + } else { + const relationshipId = url.searchParams.get("relationshipId"); + if (!relationshipId) + return NextResponse.json({ message: "Invalid relationshipId" }, { status: 400 }); + const countdown = await getCountdownByRelationship(parseInt(relationshipId)); + return NextResponse.json(countdown); + } + } catch (error) { + console.error(error); + return NextResponse.json({ message: "Error" }, { status: 500 }); + } +}; diff --git a/src/app/api/messages/fetchMessages/route.ts b/src/app/api/messages/fetchMessages/route.ts index e69de29..6518aaf 100644 --- a/src/app/api/messages/fetchMessages/route.ts +++ b/src/app/api/messages/fetchMessages/route.ts @@ -0,0 +1,26 @@ +"use server"; +import { NextResponse } from "next/server"; +import { fetchMessages } from "~/server/functions"; + +export const GET = async (request: Request) => { + try { + const url = new URL(request.url); + const apiKey = url.searchParams.get("apiKey"); + if (apiKey !== process.env.API_KEY) { + console.log("Invalid API Key"); + return NextResponse.json({ message: "Invalid API Key" }, { status: 401 }); + } else { + const userId = url.searchParams.get("userId"); + if (!userId) + return NextResponse.json({ message: "Invalid userId" }, { status: 400 }); + const partnerId = url.searchParams.get("partnerId"); + if (!partnerId) + return NextResponse.json({ message: "Invalid partnerId" }, { status: 400 }); + const messages = await fetchMessages(parseInt(userId), parseInt(partnerId)); + return NextResponse.json(messages); + } + } catch (error) { + console.error(error); + return NextResponse.json({ message: "Error" }, { status: 500 }); + } +}; diff --git a/src/app/api/relationships/createRequest/route.ts b/src/app/api/relationships/createRequest/route.ts index d917f25..51f1385 100644 --- a/src/app/api/relationships/createRequest/route.ts +++ b/src/app/api/relationships/createRequest/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { createRelationshipRequest } from "~/server/functions"; -interface CreateRequestRequest { +type CreateRequestRequest = { apiKey: string; userId: number; targetUserId: number; diff --git a/src/app/api/users/changePassword/route.ts b/src/app/api/users/changePassword/route.ts index 17435ba..84d3a23 100644 --- a/src/app/api/users/changePassword/route.ts +++ b/src/app/api/users/changePassword/route.ts @@ -21,8 +21,6 @@ export const POST = async (request: Request) => { console.log("Changing password for user:", userId); await changePassword(userId, oldPassword, newPassword); - - console.log("Password changed successfully"); return NextResponse.json({ message: "Password changed successfully" }); } catch (error) { console.error("Error in changePassword:", error); diff --git a/src/app/api/users/getUserByID/route.ts b/src/app/api/users/getUserByID/route.ts index 11a568a..990595e 100644 --- a/src/app/api/users/getUserByID/route.ts +++ b/src/app/api/users/getUserByID/route.ts @@ -10,7 +10,10 @@ export const GET = async (request: Request) => { console.log("Invalid API Key"); return NextResponse.json({ message: "Invalid API Key" }, { status: 401 }); } else { - const userId = url.searchParams.get("userId") ?? "2"; + const userId = url.searchParams.get("userId"); + if (!userId) { + return NextResponse.json({ message: "Invalid userId" }, { status: 400 }); + } const user = await getUserById(parseInt(userId)); return NextResponse.json(user); } diff --git a/src/app/api/users/login/route.ts b/src/app/api/users/login/route.ts index c85e931..1e2c2b0 100644 --- a/src/app/api/users/login/route.ts +++ b/src/app/api/users/login/route.ts @@ -1,6 +1,6 @@ "use server"; import { NextResponse } from "next/server"; -import { userLogin } from "~/server/functions"; +import { login } from "~/server/functions"; interface LoginRequest { apiKey: string; @@ -23,7 +23,7 @@ export async function POST(request: Request) { } console.log("Logging in user:", username); - const result = await userLogin(username, password); + const result = await login(username, password); if (result) { console.log("User logged in successfully"); diff --git a/src/app/api/users/logout/route.ts b/src/app/api/users/logout/route.ts new file mode 100644 index 0000000..efcea40 --- /dev/null +++ b/src/app/api/users/logout/route.ts @@ -0,0 +1,32 @@ +"use server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { logout } from "~/server/functions"; +import { middleware } from "~/middleware"; +import jwt from "jsonwebtoken"; + +export const POST = async (request: NextRequest) => { + const middlewareResponse = middleware(request); + if (middlewareResponse) return middlewareResponse; + + try { + const { token } = await request.json() as { token: string }; + if (!token) + return NextResponse.json({ message: "Token is required" },{ status: 400 }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: number }; + if (!decoded.userId) + throw new Error("Invalid token"); + await logout(decoded.userId); + return NextResponse.json({ message: "Logged out successfully" }); + } catch (jwtError) { + return NextResponse.json({ message: "Invalid token", error: jwtError }, { status: 400 }); + } + } catch (error) { + if (error instanceof Error) + return NextResponse.json({ message: error.message }, { status: 400 }); + else + return NextResponse.json({ message: "Unknown error occurred" }, { status: 500 }); + } +} diff --git a/src/app/api/users/refreshToken/route.ts b/src/app/api/users/refreshToken/route.ts new file mode 100644 index 0000000..6a01297 --- /dev/null +++ b/src/app/api/users/refreshToken/route.ts @@ -0,0 +1,22 @@ +"use server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { refreshToken } from "~/server/functions"; +import { middleware } from "~/middleware"; + +export async function POST(request: NextRequest) { + const middlewareResponse = middleware(request); + if (middlewareResponse) return middlewareResponse; + try { + const { refreshToken: token } = await request.json() as { refreshToken: string }; + if (!token) + return NextResponse.json({ message: "Refresh token is required" },{ status: 400 }); + const tokens = await refreshToken(token); + return NextResponse.json(tokens); + } catch (error) { + if (error instanceof Error) + return NextResponse.json({ message: error.message }, { status: 400 }); + else + return NextResponse.json({ message: "Unknown error occurred" }, { status: 500 }); + } +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..ebeded1 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,13 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +export function middleware(request: NextRequest) { + const apiKey = request.headers.get('x-api-key'); + + if (!apiKey || apiKey !== process.env.API_KEY) + return NextResponse.json({ message: 'Invalid API key' }, { status: 401 }); +} + +export const config = { + matcher: '/api/:path*', +}; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index f889c18..0816e3a 100755 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -24,7 +24,8 @@ export const users = createTable( passwordHash: varchar("password_hash", {length: 255}).notNull(), name: varchar("name", { length: 100 }), pfpURL: varchar("pfp_url", { length: 255 }), - pushToken: varchar("pushToken", { length: 256 }), + pushToken: varchar("pushToken", { length: 255 }), + refreshToken: varchar("refreshToken", { length: 255 }), lastLogin: timestamp("last_login", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }) .default(sql`CURRENT_TIMESTAMP`) diff --git a/src/server/functions.ts b/src/server/functions.ts index d41d3f5..af1a77e 100755 --- a/src/server/functions.ts +++ b/src/server/functions.ts @@ -3,6 +3,7 @@ import { db } from '~/server/db'; import * as schema from '~/server/db/schema'; import { eq, and, or } from 'drizzle-orm'; import { pgEnum } from 'drizzle-orm/pg-core'; +import jwt from 'jsonwebtoken'; // --- Helper Functions --- // @@ -198,19 +199,24 @@ export const updateUserPushToken = async (userId: number, pushToken: string) => } }; -export const userLogin = async (username: string, passwordHash: string) => { +export const login = async (username: string, passwordHash: string) => { try { const user = await getUserByUsername(username); if (user?.passwordHash !== passwordHash) { throw new Error("Invalid password"); } + const accessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, + {expiresIn: '15m' }); + const refreshToken = jwt.sign({ userId: user.id }, process.env.JWT_REFRESH_SECRET!, + {expiresIn: '7d' }); + // Update last login timestamp await db.update(schema.users) .set({ lastLogin: new Date() }) .where(eq(schema.users.id, user.id)); - return user; + return { user, accessToken, refreshToken }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to Log in: ${error.message}`); @@ -220,6 +226,47 @@ export const userLogin = async (username: string, passwordHash: string) => { } }; +export const refreshToken = async (refreshToken: string) => { + try { + const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as { userId: number }; + if (!decoded.userId) + throw new Error("Invalid refresh token"); + const user = await getUserById(decoded.userId); + if (!user || user.refreshToken !== refreshToken) + throw new Error("Invalid refresh token"); + const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, + {expiresIn: '15m' }); + const newRefreshToken = jwt.sign({ userId: user.id }, process.env.JWT_REFRESH_SECRET!, + {expiresIn: '7d' }); + await db.update(schema.users) + .set({ refreshToken: newRefreshToken }) + .where(eq(schema.users.id, user.id)); + + return { accessToken: newAccessToken, refreshToken: newRefreshToken }; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to refresh token: ${error.message}`); + } else { + throw new Error("Unknown error occurred while refreshing token"); + } + } +}; + +export const logout = async (userId: number) => { + try { + await db.update(schema.users) + .set({ lastLogin: null }) + .where(eq(schema.users.id, userId)); + return { success: true }; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to logout: ${error.message}`); + } else { + throw new Error("Unknown error occurred while logging out"); + } + } +}; + // --- Relationship Management Functions --- // export const createRelationshipRequest = async (requestorId: number, requestedId: number) => {