continued migration

This commit is contained in:
Michael Freno
2025-12-16 23:31:12 -05:00
parent 8fb748f401
commit b3df3eedd2
117 changed files with 16957 additions and 3172 deletions

View File

@@ -1,38 +1,35 @@
import jwt, { JwtPayload } from "jsonwebtoken";
import { cookies } from "next/headers";
import { getCookie, setCookie, type H3Event } from "vinxi/http";
import { jwtVerify, type JWTPayload, SignJWT } from "jose";
import { env } from "~/env/server";
import { createClient, Row } from "@libsql/client/web";
import { v4 as uuid } from "uuid";
import { createClient as createAPIClient } from "@tursodatabase/api";
import { OAuth2Client } from "google-auth-library";
import * as bcrypt from "bcrypt";
export const LINEAGE_JWT_EXPIRY = "14d";
export async function getPrivilegeLevel(): Promise<
"anonymous" | "admin" | "user"
> {
// Helper function to get privilege level from H3Event (for use outside tRPC)
export async function getPrivilegeLevel(
event: H3Event,
): Promise<"anonymous" | "admin" | "user"> {
try {
const userIDToken = (await cookies()).get("userIDToken");
const userIDToken = getCookie(event, "userIDToken");
if (userIDToken) {
const decoded = await new Promise<JwtPayload | undefined>((resolve) => {
jwt.verify(
userIDToken.value,
env.JWT_SECRET_KEY,
async (err, decoded) => {
if (err) {
console.log("Failed to authenticate token.");
(await cookies()).set({
name: "userIDToken",
value: "",
maxAge: 0,
expires: new Date("2016-10-05"),
});
resolve(undefined);
} else {
resolve(decoded as JwtPayload);
}
},
);
});
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(userIDToken, secret);
if (decoded) {
return decoded.id === env.ADMIN_ID ? "admin" : "user";
if (payload.id && typeof payload.id === "string") {
return payload.id === env.ADMIN_ID ? "admin" : "user";
}
} catch (err) {
console.log("Failed to authenticate token.");
setCookie(event, "userIDToken", "", {
maxAge: 0,
expires: new Date("2016-10-05"),
});
}
}
} catch (e) {
@@ -40,34 +37,26 @@ export async function getPrivilegeLevel(): Promise<
}
return "anonymous";
}
export async function getUserID(): Promise<string | null> {
// Helper function to get user ID from H3Event (for use outside tRPC)
export async function getUserID(event: H3Event): Promise<string | null> {
try {
const userIDToken = (await cookies()).get("userIDToken");
const userIDToken = getCookie(event, "userIDToken");
if (userIDToken) {
const decoded = await new Promise<JwtPayload | undefined>((resolve) => {
jwt.verify(
userIDToken.value,
env.JWT_SECRET_KEY,
async (err, decoded) => {
if (err) {
console.log("Failed to authenticate token.");
(await cookies()).set({
name: "userIDToken",
value: "",
maxAge: 0,
expires: new Date("2016-10-05"),
});
resolve(undefined);
} else {
resolve(decoded as JwtPayload);
}
},
);
});
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(userIDToken, secret);
if (decoded) {
return decoded.id;
if (payload.id && typeof payload.id === "string") {
return payload.id;
}
} catch (err) {
console.log("Failed to authenticate token.");
setCookie(event, "userIDToken", "", {
maxAge: 0,
expires: new Date("2016-10-05"),
});
}
}
} catch (e) {
@@ -76,9 +65,6 @@ export async function getUserID(): Promise<string | null> {
return null;
}
import { createClient, Row } from "@libsql/client/web";
import { env } from "@/env.mjs";
// Turso
export function ConnectionFactory() {
const config = {
@@ -100,11 +86,6 @@ export function LineageConnectionFactory() {
return conn;
}
import { v4 as uuid } from "uuid";
import { createClient as createAPIClient } from "@tursodatabase/api";
import { checkPassword } from "./api/passwordHashing";
import { OAuth2Client } from "google-auth-library";
export async function LineageDBInit() {
const turso = createAPIClient({
org: "mikefreno",
@@ -220,11 +201,13 @@ export async function validateLineageRequest({
}): Promise<boolean> {
const { provider, email } = userRow;
if (provider === "email") {
const decoded = jwt.verify(
auth_token,
env.JWT_SECRET_KEY,
) as jwt.JwtPayload;
if (email !== decoded.email) {
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(auth_token, secret);
if (email !== payload.email) {
return false;
}
} catch (err) {
return false;
}
} else if (provider == "apple") {
@@ -233,7 +216,12 @@ export async function validateLineageRequest({
return false;
}
} else if (provider == "google") {
const CLIENT_ID = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE;
// Note: Using client env var - should be available via import.meta.env in actual runtime
const CLIENT_ID = process.env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE;
if (!CLIENT_ID) {
console.error("Missing VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE");
return false;
}
const client = new OAuth2Client(CLIENT_ID);
const ticket = await client.verifyIdToken({
idToken: auth_token,
@@ -247,3 +235,110 @@ export async function validateLineageRequest({
}
return true;
}
// Password hashing utilities
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
const hashedPassword = await bcrypt.hash(password, salt);
return hashedPassword;
}
export async function checkPassword(
password: string,
hash: string
): Promise<boolean> {
const match = await bcrypt.compare(password, hash);
return match;
}
// Email service utilities
export async function sendEmailVerification(userEmail: string): Promise<{
success: boolean;
messageId?: string;
message?: string;
}> {
const apiKey = env.SENDINBLUE_KEY;
const apiUrl = "https://api.brevo.com/v3/smtp/email";
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ email: userEmail })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("15m")
.sign(secret);
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN || "https://freno.me";
const emailPayload = {
sender: {
name: "MikeFreno",
email: "lifeandlineage_no_reply@freno.me",
},
to: [
{
email: userEmail,
},
],
htmlContent: `<html>
<head>
<style>
.center {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.button {
display: inline-block;
padding: 10px 20px;
text-align: center;
text-decoration: none;
color: #ffffff;
background-color: #007BFF;
border-radius: 6px;
transition: background-color 0.3s;
}
.button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="center">
<p>Click the button below to verify email</p>
</div>
<br/>
<div class="center">
<a href="${domain}/api/lineage/email/verification/${userEmail}/?token=${token}" class="button">Verify Email</a>
</div>
</body>
</html>
`,
subject: `Life and Lineage email verification`,
};
try {
const res = await fetch(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json",
},
body: JSON.stringify(emailPayload),
});
if (!res.ok) {
return { success: false, message: "Failed to send email" };
}
const json = await res.json() as { messageId?: string };
if (json.messageId) {
return { success: true, messageId: json.messageId };
}
return { success: false, message: "No messageId in response" };
} catch (error) {
console.error("Email sending error:", error);
return { success: false, message: "Email service error" };
}
}