feat: add Nessa club events router (CRUD/RSVP/participants), suppress expected JWT verify logs, drop unused GOOGLE_CLIENT_ID env
This commit is contained in:
2
src/env/server.ts
vendored
2
src/env/server.ts
vendored
@@ -49,7 +49,6 @@ const serverEnvSchema = z.object({
|
|||||||
VITE_DOWNLOAD_BUCKET_STRING: z.string().min(1),
|
VITE_DOWNLOAD_BUCKET_STRING: z.string().min(1),
|
||||||
VITE_GOOGLE_CLIENT_ID: z.string().min(1),
|
VITE_GOOGLE_CLIENT_ID: z.string().min(1),
|
||||||
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1),
|
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1),
|
||||||
GOOGLE_CLIENT_ID: z.string().min(1),
|
|
||||||
VITE_GITHUB_CLIENT_ID: z.string().min(1),
|
VITE_GITHUB_CLIENT_ID: z.string().min(1),
|
||||||
VITE_WEBSOCKET: z.string().min(1),
|
VITE_WEBSOCKET: z.string().min(1),
|
||||||
VITE_INFILL_ENDPOINT: z.string().min(1),
|
VITE_INFILL_ENDPOINT: z.string().min(1),
|
||||||
@@ -164,7 +163,6 @@ export const getMissingEnvVars = (): string[] => {
|
|||||||
"VITE_DOWNLOAD_BUCKET_STRING",
|
"VITE_DOWNLOAD_BUCKET_STRING",
|
||||||
"VITE_GOOGLE_CLIENT_ID",
|
"VITE_GOOGLE_CLIENT_ID",
|
||||||
"VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE",
|
"VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE",
|
||||||
"GOOGLE_CLIENT_ID",
|
|
||||||
"VITE_GITHUB_CLIENT_ID",
|
"VITE_GITHUB_CLIENT_ID",
|
||||||
"VITE_WEBSOCKET",
|
"VITE_WEBSOCKET",
|
||||||
"REDIS_URL",
|
"REDIS_URL",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { createTRPCRouter, publicProcedure, csrfProtectedProcedure } from "../../utils";
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
publicProcedure,
|
||||||
|
csrfProtectedProcedure
|
||||||
|
} from "../../utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
LineageConnectionFactory,
|
LineageConnectionFactory,
|
||||||
@@ -6,7 +10,7 @@ import {
|
|||||||
hashPassword,
|
hashPassword,
|
||||||
checkPassword,
|
checkPassword,
|
||||||
sendEmailVerification,
|
sendEmailVerification,
|
||||||
LINEAGE_JWT_EXPIRY,
|
LINEAGE_JWT_EXPIRY
|
||||||
} from "~/server/utils";
|
} from "~/server/utils";
|
||||||
import { env } from "~/env/server";
|
import { env } from "~/env/server";
|
||||||
import { LINEAGE_CONFIG } from "~/config";
|
import { LINEAGE_CONFIG } from "~/config";
|
||||||
@@ -20,7 +24,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(8),
|
password: z.string().min(8)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -34,7 +38,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid Credentials",
|
message: "Invalid Credentials"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +47,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (user.email_verified === 0) {
|
if (user.email_verified === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Email not yet verified!",
|
message: "Email not yet verified!"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +55,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid Credentials",
|
message: "Invalid Credentials"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +74,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
success: true,
|
success: true,
|
||||||
message: "Login successful",
|
message: "Login successful",
|
||||||
token,
|
token,
|
||||||
email,
|
email
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(8),
|
password: z.string().min(8),
|
||||||
password_conf: z.string().min(8),
|
password_conf: z.string().min(8)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -88,7 +92,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (password !== password_conf) {
|
if (password !== password_conf) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Password mismatch",
|
message: "Password mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +111,12 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (emailResult.success && emailResult.messageId) {
|
if (emailResult.success && emailResult.messageId) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email verification sent!",
|
message: "Email verification sent!"
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: emailResult.message || "Failed to send verification email",
|
message: emailResult.message || "Failed to send verification email"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -120,13 +124,13 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (e instanceof LibsqlError && e.code === "SQLITE_CONSTRAINT") {
|
if (e instanceof LibsqlError && e.code === "SQLITE_CONSTRAINT") {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "User already exists",
|
message: "User already exists"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (e instanceof TRPCError) throw e;
|
if (e instanceof TRPCError) throw e;
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while creating the user",
|
message: "An error occurred while creating the user"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -135,7 +139,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
token: z.string(),
|
token: z.string()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -152,13 +156,13 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
const secret = new TextEncoder().encode(env.LINEAGE_JWT_SECRET);
|
const secret = new TextEncoder().encode(env.LINEAGE_JWT_SECRET);
|
||||||
const { payload } = await jwtVerify(token, secret, {
|
const { payload } = await jwtVerify(token, secret, {
|
||||||
issuer: LINEAGE_CONFIG.JWT_ISSUER,
|
issuer: LINEAGE_CONFIG.JWT_ISSUER,
|
||||||
audience: LINEAGE_CONFIG.JWT_AUDIENCE,
|
audience: LINEAGE_CONFIG.JWT_AUDIENCE
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.email !== userEmail) {
|
if (payload.email !== userEmail) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Authentication failed: email mismatch",
|
message: "Authentication failed: email mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +182,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message:
|
||||||
"Email verification success. You may close this window and sign in within the app.",
|
"Email verification success. You may close this window and sign in within the app."
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in email verification:", err);
|
console.error("Error in email verification:", err);
|
||||||
@@ -187,7 +191,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const turso = createAPIClient({
|
const turso = createAPIClient({
|
||||||
org: "mikefreno",
|
org: "mikefreno",
|
||||||
token: env.TURSO_DB_API_TOKEN,
|
token: env.TURSO_DB_API_TOKEN
|
||||||
});
|
});
|
||||||
await turso.databases.delete(dbName);
|
await turso.databases.delete(dbName);
|
||||||
console.log(`Database ${dbName} deleted due to error`);
|
console.log(`Database ${dbName} deleted due to error`);
|
||||||
@@ -200,7 +204,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`,
|
sql: `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`,
|
||||||
args: [false, null, null, userEmail],
|
args: [false, null, null, userEmail]
|
||||||
});
|
});
|
||||||
console.log("User table update reverted");
|
console.log("User table update reverted");
|
||||||
} catch (revertErr) {
|
} catch (revertErr) {
|
||||||
@@ -212,7 +216,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message:
|
message:
|
||||||
"Authentication failed: An error occurred during email verification. Please try again.",
|
"Authentication failed: An error occurred during email verification. Please try again."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -230,7 +234,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (res.rows.length === 0 || res.rows[0].email_verified) {
|
if (res.rows.length === 0 || res.rows[0].email_verified) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "CONFLICT",
|
code: "CONFLICT",
|
||||||
message: "Invalid Request",
|
message: "Invalid Request"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,12 +242,12 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (emailResult.success && emailResult.messageId) {
|
if (emailResult.success && emailResult.messageId) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email verification sent!",
|
message: "Email verification sent!"
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: emailResult.message || "Failed to send verification email",
|
message: emailResult.message || "Failed to send verification email"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -257,12 +261,12 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
const secret = new TextEncoder().encode(env.LINEAGE_JWT_SECRET);
|
const secret = new TextEncoder().encode(env.LINEAGE_JWT_SECRET);
|
||||||
const { payload } = await jwtVerify(token, secret, {
|
const { payload } = await jwtVerify(token, secret, {
|
||||||
issuer: LINEAGE_CONFIG.JWT_ISSUER,
|
issuer: LINEAGE_CONFIG.JWT_ISSUER,
|
||||||
audience: LINEAGE_CONFIG.JWT_AUDIENCE,
|
audience: LINEAGE_CONFIG.JWT_AUDIENCE
|
||||||
});
|
});
|
||||||
|
|
||||||
const newToken = await new SignJWT({
|
const newToken = await new SignJWT({
|
||||||
userId: payload.userId,
|
userId: payload.userId,
|
||||||
email: payload.email,
|
email: payload.email
|
||||||
})
|
})
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setIssuer(LINEAGE_CONFIG.JWT_ISSUER)
|
.setIssuer(LINEAGE_CONFIG.JWT_ISSUER)
|
||||||
@@ -275,12 +279,12 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
ok: true,
|
ok: true,
|
||||||
valid: true,
|
valid: true,
|
||||||
token: newToken,
|
token: newToken,
|
||||||
email: payload.email,
|
email: payload.email
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid or expired token",
|
message: "Invalid or expired token"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -296,7 +300,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
const checkUserQuery = "SELECT * FROM User WHERE email = ?";
|
const checkUserQuery = "SELECT * FROM User WHERE email = ?";
|
||||||
const checkUserResult = await conn.execute({
|
const checkUserResult = await conn.execute({
|
||||||
sql: checkUserQuery,
|
sql: checkUserQuery,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (checkUserResult.rows.length > 0) {
|
if (checkUserResult.rows.length > 0) {
|
||||||
@@ -307,18 +311,18 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
`;
|
`;
|
||||||
const updateRes = await conn.execute({
|
const updateRes = await conn.execute({
|
||||||
sql: updateQuery,
|
sql: updateQuery,
|
||||||
args: ["google", email],
|
args: ["google", email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateRes.rowsAffected !== 0) {
|
if (updateRes.rowsAffected !== 0) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "User information updated",
|
message: "User information updated"
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "User update failed!",
|
message: "User update failed!"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -333,27 +337,27 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
`;
|
`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: insertQuery,
|
sql: insertQuery,
|
||||||
args: [email, true, "google", dbName, token],
|
args: [email, true, "google", dbName, token]
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("insert success");
|
console.log("insert success");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "New user created",
|
message: "New user created"
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (db_name) {
|
if (db_name) {
|
||||||
const turso = createAPIClient({
|
const turso = createAPIClient({
|
||||||
org: "mikefreno",
|
org: "mikefreno",
|
||||||
token: env.TURSO_DB_API_TOKEN,
|
token: env.TURSO_DB_API_TOKEN
|
||||||
});
|
});
|
||||||
await turso.databases.delete(db_name);
|
await turso.databases.delete(db_name);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to create user",
|
message: "Failed to create user"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,7 +366,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (error instanceof TRPCError) throw error;
|
if (error instanceof TRPCError) throw error;
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while processing the request",
|
message: "An error occurred while processing the request"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -371,7 +375,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email().optional(),
|
email: z.string().email().optional(),
|
||||||
idToken: z.string(),
|
idToken: z.string()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -384,7 +388,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (!appleKeysResponse.ok) {
|
if (!appleKeysResponse.ok) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to fetch Apple public keys",
|
message: "Failed to fetch Apple public keys"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +408,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (!headerB64) {
|
if (!headerB64) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid Apple ID token format",
|
message: "Invalid Apple ID token format"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const headerJson = Buffer.from(headerB64, "base64url").toString("utf8");
|
const headerJson = Buffer.from(headerB64, "base64url").toString("utf8");
|
||||||
@@ -413,14 +417,14 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (!jwk) {
|
if (!jwk) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Apple public key not found",
|
message: "Apple public key not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKey = await importJWK(jwk, "RS256");
|
const publicKey = await importJWK(jwk, "RS256");
|
||||||
const jwtOptions: Parameters<typeof jwtVerify>[2] = {
|
const jwtOptions: Parameters<typeof jwtVerify>[2] = {
|
||||||
algorithms: ["RS256"],
|
algorithms: ["RS256"],
|
||||||
issuer: "https://appleid.apple.com",
|
issuer: "https://appleid.apple.com"
|
||||||
};
|
};
|
||||||
if (env.APPLE_CLIENT_ID) {
|
if (env.APPLE_CLIENT_ID) {
|
||||||
jwtOptions.audience = env.APPLE_CLIENT_ID;
|
jwtOptions.audience = env.APPLE_CLIENT_ID;
|
||||||
@@ -441,14 +445,14 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
let checkUserQuery = "SELECT * FROM User WHERE apple_user_string = ?";
|
let checkUserQuery = "SELECT * FROM User WHERE apple_user_string = ?";
|
||||||
|
|
||||||
let args: string[] = [userString];
|
const args: string[] = [userString];
|
||||||
if (email) {
|
if (email) {
|
||||||
args.push(email);
|
args.push(email);
|
||||||
checkUserQuery += " OR email = ?";
|
checkUserQuery += " OR email = ?";
|
||||||
}
|
}
|
||||||
const checkUserResult = await conn.execute({
|
const checkUserResult = await conn.execute({
|
||||||
sql: checkUserQuery,
|
sql: checkUserQuery,
|
||||||
args: args,
|
args: args
|
||||||
});
|
});
|
||||||
|
|
||||||
if (checkUserResult.rows.length > 0) {
|
if (checkUserResult.rows.length > 0) {
|
||||||
@@ -474,19 +478,19 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
)} ${whereClause}`;
|
)} ${whereClause}`;
|
||||||
const updateRes = await conn.execute({
|
const updateRes = await conn.execute({
|
||||||
sql: updateQuery,
|
sql: updateQuery,
|
||||||
args: values,
|
args: values
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateRes.rowsAffected !== 0) {
|
if (updateRes.rowsAffected !== 0) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "User information updated",
|
message: "User information updated",
|
||||||
email: checkUserResult.rows[0].email as string,
|
email: checkUserResult.rows[0].email as string
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "User update failed!",
|
message: "User update failed!"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -501,27 +505,27 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
`;
|
`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: insertQuery,
|
sql: insertQuery,
|
||||||
args: [email, true, userString, "apple", dbName, dbToken],
|
args: [email, true, userString, "apple", dbName, dbToken]
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "New user created",
|
message: "New user created",
|
||||||
dbName,
|
dbName,
|
||||||
dbToken,
|
dbToken
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (dbName) {
|
if (dbName) {
|
||||||
const turso = createAPIClient({
|
const turso = createAPIClient({
|
||||||
org: "mikefreno",
|
org: "mikefreno",
|
||||||
token: env.TURSO_DB_API_TOKEN,
|
token: env.TURSO_DB_API_TOKEN
|
||||||
});
|
});
|
||||||
await turso.databases.delete(dbName);
|
await turso.databases.delete(dbName);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to create user",
|
message: "Failed to create user"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,7 +534,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const turso = createAPIClient({
|
const turso = createAPIClient({
|
||||||
org: "mikefreno",
|
org: "mikefreno",
|
||||||
token: env.TURSO_DB_API_TOKEN,
|
token: env.TURSO_DB_API_TOKEN
|
||||||
});
|
});
|
||||||
await turso.databases.delete(dbName);
|
await turso.databases.delete(dbName);
|
||||||
} catch (deleteErr) {
|
} catch (deleteErr) {
|
||||||
@@ -541,7 +545,7 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
if (error instanceof TRPCError) throw error;
|
if (error instanceof TRPCError) throw error;
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "An error occurred while processing the request",
|
message: "An error occurred while processing the request"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -560,8 +564,8 @@ export const lineageAuthRouter = createTRPCRouter({
|
|||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1272,5 +1272,333 @@ export const nessaCommunityRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Events (clubEvents)
|
||||||
|
// ==========================================================================
|
||||||
|
events: createTRPCRouter({
|
||||||
|
list: nessaProcedure
|
||||||
|
.input(paginationSchema.extend({
|
||||||
|
clubId: z.string().min(1).optional(),
|
||||||
|
eventType: z.string().optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
location: z.string().optional(),
|
||||||
|
rsvpStatus: z.enum(["going", "maybe", "not-going"]).optional()
|
||||||
|
}))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const limit = input.limit ?? 50;
|
||||||
|
const offset = input.offset ?? 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
const where: string[] = [];
|
||||||
|
const args: (string | number)[] = [];
|
||||||
|
|
||||||
|
if (input.clubId) {
|
||||||
|
where.push("e.clubId = ?");
|
||||||
|
args.push(input.clubId);
|
||||||
|
}
|
||||||
|
if (input.eventType) {
|
||||||
|
where.push("e.eventType = ?");
|
||||||
|
args.push(input.eventType);
|
||||||
|
}
|
||||||
|
if (input.startDate) {
|
||||||
|
where.push("e.startDate >= ?");
|
||||||
|
args.push(input.startDate);
|
||||||
|
}
|
||||||
|
if (input.endDate) {
|
||||||
|
where.push("e.startDate <= ?");
|
||||||
|
args.push(input.endDate);
|
||||||
|
}
|
||||||
|
if (input.location) {
|
||||||
|
where.push("(e.location LIKE ?)");
|
||||||
|
args.push(`%${input.location}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = where.length
|
||||||
|
? `WHERE ${where.join(" AND ")}`
|
||||||
|
: "";
|
||||||
|
args.push(limit, offset);
|
||||||
|
|
||||||
|
const result = await conn.execute({
|
||||||
|
sql: `SELECT e.id, e.clubId, e.title, e.description, e.eventType,
|
||||||
|
e.location, e.latitude, e.longitude, e.startDate, e.endDate,
|
||||||
|
e.createdBy, e.maxParticipants, e.participantCount,
|
||||||
|
e.createdAt, e.updatedAt,
|
||||||
|
u.displayName AS creatorDisplayName, u.avatarUrl AS creatorAvatarUrl,
|
||||||
|
(SELECT COUNT(*) FROM clubEventRSVPs WHERE eventId = e.id) AS rsvpCount,
|
||||||
|
(SELECT status FROM clubEventRSVPs WHERE eventId = e.id AND userId = ?) AS userRsvpStatus
|
||||||
|
FROM clubEvents e
|
||||||
|
JOIN users u ON e.createdBy = u.id
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY e.startDate ASC LIMIT ? OFFSET ?`,
|
||||||
|
args: [...args, ctx.nessaUserId]
|
||||||
|
});
|
||||||
|
|
||||||
|
return { events: result.rows };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to list Nessa events:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to list events"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
get: nessaProcedure
|
||||||
|
.input(idSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
const result = await conn.execute({
|
||||||
|
sql: `SELECT e.id, e.clubId, e.title, e.description, e.eventType,
|
||||||
|
e.location, e.latitude, e.longitude, e.startDate, e.endDate,
|
||||||
|
e.createdBy, e.maxParticipants, e.participantCount,
|
||||||
|
e.createdAt, e.updatedAt,
|
||||||
|
u.displayName AS creatorDisplayName, u.avatarUrl AS creatorAvatarUrl,
|
||||||
|
(SELECT COUNT(*) FROM clubEventRSVPs WHERE eventId = e.id) AS rsvpCount,
|
||||||
|
(SELECT status FROM clubEventRSVPs WHERE eventId = e.id AND userId = ?) AS userRsvpStatus
|
||||||
|
FROM clubEvents e
|
||||||
|
JOIN users u ON e.createdBy = u.id
|
||||||
|
WHERE e.id = ?`,
|
||||||
|
args: [ctx.nessaUserId, input.id]
|
||||||
|
});
|
||||||
|
if (!result.rows.length) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Event not found" });
|
||||||
|
}
|
||||||
|
return { event: result.rows[0] };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to get Nessa event:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to get event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
create: nessaProcedure
|
||||||
|
.input(z.object({
|
||||||
|
clubId: z.string().min(1),
|
||||||
|
title: z.string().min(1).max(200),
|
||||||
|
description: z.string().max(2000).nullable().optional(),
|
||||||
|
eventType: z.string().min(1),
|
||||||
|
location: z.string().nullable().optional(),
|
||||||
|
latitude: z.number().nullable().optional(),
|
||||||
|
longitude: z.number().nullable().optional(),
|
||||||
|
startDate: z.string().min(1),
|
||||||
|
endDate: z.string().nullable().optional(),
|
||||||
|
maxParticipants: z.number().int().min(1).nullable().optional()
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
await requireClubMembership(conn, input.clubId, ctx.nessaUserId);
|
||||||
|
|
||||||
|
const eventId = crypto.randomUUID();
|
||||||
|
await conn.execute({
|
||||||
|
sql: `INSERT INTO clubEvents
|
||||||
|
(id, clubId, title, description, eventType, location, latitude, longitude,
|
||||||
|
startDate, endDate, createdBy, maxParticipants)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
args: [
|
||||||
|
eventId,
|
||||||
|
input.clubId,
|
||||||
|
input.title,
|
||||||
|
input.description ?? null,
|
||||||
|
input.eventType,
|
||||||
|
input.location ?? null,
|
||||||
|
input.latitude ?? null,
|
||||||
|
input.longitude ?? null,
|
||||||
|
input.startDate,
|
||||||
|
input.endDate ?? null,
|
||||||
|
ctx.nessaUserId,
|
||||||
|
input.maxParticipants ?? null
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, eventId };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to create Nessa event:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to create event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: nessaProcedure
|
||||||
|
.input(z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
title: z.string().min(1).max(200).optional(),
|
||||||
|
description: z.string().max(2000).nullable().optional(),
|
||||||
|
eventType: z.string().min(1).optional(),
|
||||||
|
location: z.string().nullable().optional(),
|
||||||
|
latitude: z.number().nullable().optional(),
|
||||||
|
longitude: z.number().nullable().optional(),
|
||||||
|
startDate: z.string().min(1).optional(),
|
||||||
|
endDate: z.string().nullable().optional(),
|
||||||
|
maxParticipants: z.number().int().min(1).nullable().optional()
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
|
||||||
|
const ownerCheck = await conn.execute({
|
||||||
|
sql: "SELECT createdBy FROM clubEvents WHERE id = ?",
|
||||||
|
args: [input.id]
|
||||||
|
});
|
||||||
|
if (!ownerCheck.rows.length) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Event not found" });
|
||||||
|
}
|
||||||
|
if ((ownerCheck.rows[0] as unknown as { createdBy: string }).createdBy !== ctx.nessaUserId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Only the creator can update the event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields: string[] = [];
|
||||||
|
const args: (string | number | null)[] = [];
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
title: "title",
|
||||||
|
description: "description",
|
||||||
|
eventType: "eventType",
|
||||||
|
location: "location",
|
||||||
|
latitude: "latitude",
|
||||||
|
longitude: "longitude",
|
||||||
|
startDate: "startDate",
|
||||||
|
endDate: "endDate",
|
||||||
|
maxParticipants: "maxParticipants"
|
||||||
|
};
|
||||||
|
for (const [key, col] of Object.entries(map)) {
|
||||||
|
if ((input as Record<string, unknown>)[key] !== undefined) {
|
||||||
|
fields.push(`${col} = ?`);
|
||||||
|
args.push((input as Record<string, unknown>)[key] as string | number | null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
fields.push("updatedAt = datetime('now')");
|
||||||
|
args.push(input.id);
|
||||||
|
|
||||||
|
await conn.execute({
|
||||||
|
sql: `UPDATE clubEvents SET ${fields.join(", ")} WHERE id = ?`,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to update Nessa event:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to update event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: nessaProcedure
|
||||||
|
.input(idSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
const ownerCheck = await conn.execute({
|
||||||
|
sql: "SELECT createdBy FROM clubEvents WHERE id = ?",
|
||||||
|
args: [input.id]
|
||||||
|
});
|
||||||
|
if (!ownerCheck.rows.length) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Event not found" });
|
||||||
|
}
|
||||||
|
if ((ownerCheck.rows[0] as unknown as { createdBy: string }).createdBy !== ctx.nessaUserId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Only the creator can delete the event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await conn.execute({
|
||||||
|
sql: "DELETE FROM clubEvents WHERE id = ?",
|
||||||
|
args: [input.id]
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to delete Nessa event:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to delete event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
rsvp: nessaProcedure
|
||||||
|
.input(z.object({
|
||||||
|
eventId: z.string().min(1),
|
||||||
|
status: z.enum(["going", "maybe", "not-going"]).default("going")
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
|
||||||
|
const event = await conn.execute({
|
||||||
|
sql: "SELECT clubId FROM clubEvents WHERE id = ?",
|
||||||
|
args: [input.eventId]
|
||||||
|
});
|
||||||
|
if (!event.rows.length) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Event not found" });
|
||||||
|
}
|
||||||
|
const clubId = (event.rows[0] as unknown as { clubId: string }).clubId;
|
||||||
|
await requireClubMembership(conn, clubId, ctx.nessaUserId);
|
||||||
|
|
||||||
|
// Delete existing RSVP if any
|
||||||
|
await conn.execute({
|
||||||
|
sql: "DELETE FROM clubEventRSVPs WHERE eventId = ? AND userId = ?",
|
||||||
|
args: [input.eventId, ctx.nessaUserId]
|
||||||
|
});
|
||||||
|
|
||||||
|
await conn.execute({
|
||||||
|
sql: "INSERT INTO clubEventRSVPs (id, eventId, userId, status) VALUES (?, ?, ?, ?)",
|
||||||
|
args: [crypto.randomUUID(), input.eventId, ctx.nessaUserId, input.status]
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, eventId: input.eventId, userId: ctx.nessaUserId, status: input.status };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to RSVP to Nessa event:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to RSVP to event"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
participants: nessaProcedure
|
||||||
|
.input(idSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const conn = NessaConnectionFactory();
|
||||||
|
const result = await conn.execute({
|
||||||
|
sql: `SELECT r.id, r.eventId, r.userId, r.status, r.createdAt,
|
||||||
|
u.firstName, u.lastName, u.displayName, u.avatarUrl
|
||||||
|
FROM clubEventRSVPs r
|
||||||
|
JOIN users u ON r.userId = u.id
|
||||||
|
WHERE r.eventId = ?
|
||||||
|
ORDER BY r.createdAt ASC`,
|
||||||
|
args: [input.id]
|
||||||
|
});
|
||||||
|
return { participants: result.rows };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) throw error;
|
||||||
|
console.error("Failed to list event participants:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to list participants"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import type { Row } from "@libsql/client/web";
|
|||||||
import { SignJWT, jwtVerify } from "jose";
|
import { SignJWT, jwtVerify } from "jose";
|
||||||
import { env } from "~/env/server";
|
import { env } from "~/env/server";
|
||||||
import { ConnectionFactory } from "./db-connections";
|
import { ConnectionFactory } from "./db-connections";
|
||||||
import { AUTH_CONFIG, LINEAGE_CONFIG, expiryToSeconds, getAccessTokenExpiry } from "~/config";
|
import {
|
||||||
|
AUTH_CONFIG,
|
||||||
|
LINEAGE_CONFIG,
|
||||||
|
expiryToSeconds,
|
||||||
|
getAccessTokenExpiry
|
||||||
|
} from "~/config";
|
||||||
|
|
||||||
export const authCookieName = "auth_token";
|
export const authCookieName = "auth_token";
|
||||||
|
|
||||||
@@ -65,7 +70,17 @@ export async function verifyAuthToken(
|
|||||||
exp: payload.exp
|
exp: payload.exp
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Auth token verification failed:", error);
|
// Signature mismatch, expired token, malformed JWT — these are expected
|
||||||
|
// when a user has a stale/invalid cookie and are NOT server errors. Only
|
||||||
|
// log unexpected failures to keep prod console noise-free.
|
||||||
|
const code = (error as { code?: string }).code;
|
||||||
|
if (
|
||||||
|
code !== "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" &&
|
||||||
|
code !== "ERR_JWT_EXPIRED" &&
|
||||||
|
code !== "ERR_JWT_MALFORMED"
|
||||||
|
) {
|
||||||
|
console.error("Auth token verification failed:", error);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user