More stuff

This commit is contained in:
Gabriel Brown 2024-10-04 16:56:32 -05:00
parent edffe130a5
commit 7371cc8851
14 changed files with 299 additions and 11 deletions

View File

@ -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",

103
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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 });
}
};

View File

@ -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 });
}
};

View File

@ -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 });
}
};

View File

@ -2,7 +2,7 @@
import { NextResponse } from "next/server";
import { createRelationshipRequest } from "~/server/functions";
interface CreateRequestRequest {
type CreateRequestRequest = {
apiKey: string;
userId: number;
targetUserId: number;

View File

@ -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);

View File

@ -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);
}

View File

@ -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");

View File

@ -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 });
}
}

View File

@ -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 });
}
}

13
src/middleware.ts Normal file
View File

@ -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*',
};

View File

@ -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`)

View File

@ -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) => {