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,34 +1,326 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { v4 as uuidV4 } from "uuid";
import { env } from "~/env/server";
import { ConnectionFactory } from "~/server/utils";
import { SignJWT, jwtVerify } from "jose";
import { setCookie } from "vinxi/http";
// Helper to create JWT token
async function createJWT(userId: string): Promise<string> {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("14d") // 14 days
.sign(secret);
return token;
}
// User type for database rows
interface User {
id: string;
email?: string;
display_name?: string;
provider?: string;
image?: string;
email_verified?: boolean;
}
export const authRouter = createTRPCRouter({
// GitHub callback route
githubCallback: publicProcedure
.query(async () => {
// Implementation for GitHub OAuth callback
return { message: "GitHub callback endpoint" };
.input(z.object({ code: z.string() }))
.mutation(async ({ input, ctx }) => {
const { code } = input;
try {
// Exchange code for access token
const tokenResponse = await fetch(
"https://github.com/login/oauth/access_token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
client_id: env.VITE_GITHUB_CLIENT_ID || env.NEXT_PUBLIC_GITHUB_CLIENT_ID,
client_secret: env.GITHUB_CLIENT_SECRET,
code,
}),
},
);
const { access_token } = await tokenResponse.json();
// Fetch user info from GitHub
const userResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `token ${access_token}`,
},
});
const user = await userResponse.json();
const login = user.login;
const conn = ConnectionFactory();
// Check if user exists
const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`;
const params = ["github", login];
const res = await conn.execute({ sql: query, args: params });
let userId: string;
if (res.rows[0]) {
// User exists
userId = (res.rows[0] as unknown as User).id;
} else {
// Create new user
const icon = user.avatar_url;
const email = user.email;
userId = uuidV4();
const insertQuery = `INSERT INTO User (id, email, display_name, provider, image) VALUES (?, ?, ?, ?, ?)`;
const insertParams = [userId, email, login, "github", icon];
await conn.execute({ sql: insertQuery, args: insertParams });
}
// Create JWT token
const token = await createJWT(userId);
// Set cookie
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
maxAge: 60 * 60 * 24 * 14, // 14 days
path: "/",
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax",
});
return {
success: true,
redirectTo: "/account",
};
} catch (error) {
console.error("GitHub authentication failed:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "GitHub authentication failed",
});
}
}),
// Google callback route
googleCallback: publicProcedure
.query(async () => {
// Implementation for Google OAuth callback
return { message: "Google callback endpoint" };
.input(z.object({ code: z.string() }))
.mutation(async ({ input, ctx }) => {
const { code } = input;
try {
// Exchange code for access token
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
code: code,
client_id: env.VITE_GOOGLE_CLIENT_ID || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "",
client_secret: env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN}/api/auth/callback/google`,
grant_type: "authorization_code",
}),
});
const { access_token } = await tokenResponse.json();
// Fetch user info from Google
const userResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
{
headers: {
Authorization: `Bearer ${access_token}`,
},
},
);
const userData = await userResponse.json();
const name = userData.name;
const image = userData.picture;
const email = userData.email;
const email_verified = userData.email_verified;
const conn = ConnectionFactory();
// Check if user exists
const query = `SELECT * FROM User WHERE provider = ? AND email = ?`;
const params = ["google", email];
const res = await conn.execute({ sql: query, args: params });
let userId: string;
if (res.rows[0]) {
// User exists
userId = (res.rows[0] as unknown as User).id;
} else {
// Create new user
userId = uuidV4();
const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`;
const insertParams = [
userId,
email,
email_verified,
name,
"google",
image,
];
await conn.execute({
sql: insertQuery,
args: insertParams,
});
}
// Create JWT token
const token = await createJWT(userId);
// Set cookie
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
maxAge: 60 * 60 * 24 * 14, // 14 days
path: "/",
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax",
});
return {
success: true,
redirectTo: "/account",
};
} catch (error) {
console.error("Google authentication failed:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Google authentication failed",
});
}
}),
// Email login route
emailLogin: publicProcedure
.input(z.object({ email: z.string().email() }))
.mutation(async ({ input }) => {
// Implementation for email login
return { message: `Email login initiated for ${input.email}` };
.input(
z.object({
email: z.string().email(),
token: z.string(),
rememberMe: z.boolean().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const { email, token, rememberMe } = input;
try {
// Verify JWT token
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(token, secret);
// Check if email matches
if (payload.email !== email) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Email mismatch",
});
}
const conn = ConnectionFactory();
const query = `SELECT * FROM User WHERE email = ?`;
const params = [email];
const res = await conn.execute({ sql: query, args: params });
if (!res.rows[0]) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const userId = (res.rows[0] as unknown as User).id;
// Create JWT token
const userToken = await createJWT(userId);
// Set cookie based on rememberMe flag
const cookieOptions: any = {
path: "/",
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax",
};
if (rememberMe) {
cookieOptions.maxAge = 60 * 60 * 24 * 14; // 14 days
}
// If rememberMe is false, cookie will be session-only (no maxAge)
setCookie(ctx.event.nativeEvent, "userIDToken", userToken, cookieOptions);
return {
success: true,
redirectTo: "/account",
};
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
console.error("Email login failed:", error);
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication failed",
});
}
}),
// Email verification route
emailVerification: publicProcedure
.input(z.object({ email: z.string().email() }))
.query(async ({ input }) => {
// Implementation for email verification
return { message: `Email verification requested for ${input.email}` };
.input(
z.object({
email: z.string().email(),
token: z.string(),
}),
)
.mutation(async ({ input }) => {
const { email, token } = input;
try {
// Verify JWT token
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(token, secret);
// Check if email matches
if (payload.email !== email) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Email mismatch",
});
}
const conn = ConnectionFactory();
const query = `UPDATE User SET email_verified = ? WHERE email = ?`;
const params = [true, email];
await conn.execute({ sql: query, args: params });
return {
success: true,
message: "Email verification success, you may close this window",
};
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
console.error("Email verification failed:", error);
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid token",
});
}
}),
});

View File

@@ -1,101 +1,563 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
import { ConnectionFactory } from "~/server/utils";
import { TRPCError } from "@trpc/server";
import { env } from "~/env/server";
export const databaseRouter = createTRPCRouter({
// Comment reactions routes
// ============================================================
// Comment Reactions Routes
// ============================================================
getCommentReactions: publicProcedure
.input(z.object({ commentId: z.string() }))
.query(({ input }) => {
// Implementation for getting comment reactions
return { commentId: input.commentId, reactions: [] };
}),
postCommentReaction: publicProcedure
.input(z.object({
commentId: z.string(),
reactionType: z.string()
}))
.mutation(({ input }) => {
// Implementation for posting comment reaction
return { success: true, commentId: input.commentId };
}),
deleteCommentReaction: publicProcedure
.input(z.object({
commentId: z.string(),
reactionType: z.string()
}))
.mutation(({ input }) => {
// Implementation for deleting comment reaction
return { success: true, commentId: input.commentId };
.input(z.object({ commentID: z.string() }))
.query(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = "SELECT * FROM CommentReaction WHERE comment_id = ?";
const results = await conn.execute({
sql: query,
args: [input.commentID],
});
return { commentReactions: results.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch comment reactions",
});
}
}),
// Comments routes
getComments: publicProcedure
.input(z.object({ postId: z.string() }))
.query(({ input }) => {
// Implementation for getting comments
return { postId: input.postId, comments: [] };
addCommentReaction: publicProcedure
.input(z.object({
type: z.string(),
comment_id: z.string(),
user_id: z.string(),
}))
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `
INSERT INTO CommentReaction (type, comment_id, user_id)
VALUES (?, ?, ?)
`;
await conn.execute({
sql: query,
args: [input.type, input.comment_id, input.user_id],
});
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
const res = await conn.execute({
sql: followUpQuery,
args: [input.comment_id],
});
return { commentReactions: res.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to add comment reaction",
});
}
}),
// Post manipulation routes
getPosts: publicProcedure
.input(z.object({
limit: z.number().optional(),
offset: z.number().optional()
removeCommentReaction: publicProcedure
.input(z.object({
type: z.string(),
comment_id: z.string(),
user_id: z.string(),
}))
.query(({ input }) => {
// Implementation for getting posts
return { posts: [], total: 0 };
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `
DELETE FROM CommentReaction
WHERE type = ? AND comment_id = ? AND user_id = ?
`;
await conn.execute({
sql: query,
args: [input.type, input.comment_id, input.user_id],
});
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
const res = await conn.execute({
sql: followUpQuery,
args: [input.comment_id],
});
return { commentReactions: res.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to remove comment reaction",
});
}
}),
// ============================================================
// Comments Routes
// ============================================================
getAllComments: publicProcedure
.query(async () => {
try {
const conn = ConnectionFactory();
const query = `SELECT * FROM Comment`;
const res = await conn.execute(query);
return { comments: res.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch comments",
});
}
}),
getCommentsByPostId: publicProcedure
.input(z.object({ post_id: z.string() }))
.query(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `SELECT * FROM Comment WHERE post_id = ?`;
const res = await conn.execute({
sql: query,
args: [input.post_id],
});
return { comments: res.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch comments by post ID",
});
}
}),
// ============================================================
// Post Routes
// ============================================================
getPostById: publicProcedure
.input(z.object({
category: z.enum(["blog", "project"]),
id: z.number(),
}))
.query(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `SELECT * FROM Post WHERE id = ?`;
const results = await conn.execute({
sql: query,
args: [input.id],
});
const tagQuery = `SELECT * FROM Tag WHERE post_id = ?`;
const tagRes = await conn.execute({
sql: tagQuery,
args: [input.id],
});
if (results.rows[0]) {
return {
post: results.rows[0],
tags: tagRes.rows,
};
} else {
return { post: null, tags: [] };
}
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch post by ID",
});
}
}),
getPostByTitle: publicProcedure
.input(z.object({
category: z.enum(["blog", "project"]),
title: z.string(),
}))
.query(async ({ input, ctx }) => {
try {
const conn = ConnectionFactory();
// Get post by title
const postQuery = "SELECT * FROM Post WHERE title = ? AND category = ? AND published = ?";
const postResults = await conn.execute({
sql: postQuery,
args: [input.title, input.category, true],
});
if (!postResults.rows[0]) {
return null;
}
const post_id = (postResults.rows[0] as any).id;
// Get comments
const commentQuery = "SELECT * FROM Comment WHERE post_id = ?";
const commentResults = await conn.execute({
sql: commentQuery,
args: [post_id],
});
// Get likes
const likeQuery = "SELECT * FROM PostLike WHERE post_id = ?";
const likeResults = await conn.execute({
sql: likeQuery,
args: [post_id],
});
// Get tags
const tagsQuery = "SELECT * FROM Tag WHERE post_id = ?";
const tagResults = await conn.execute({
sql: tagsQuery,
args: [post_id],
});
return {
project: postResults.rows[0],
comments: commentResults.rows,
likes: likeResults.rows,
tagResults: tagResults.rows,
};
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch post by title",
});
}
}),
createPost: publicProcedure
.input(z.object({
title: z.string(),
content: z.string()
.input(z.object({
category: z.enum(["blog", "project"]),
title: z.string(),
subtitle: z.string().nullable(),
body: z.string().nullable(),
banner_photo: z.string().nullable(),
published: z.boolean(),
tags: z.array(z.string()).nullable(),
author_id: z.string(),
}))
.mutation(({ input }) => {
// Implementation for creating post
return { success: true, post: { id: "1", ...input } };
}),
updatePost: publicProcedure
.input(z.object({
id: z.string(),
title: z.string().optional(),
content: z.string().optional()
}))
.mutation(({ input }) => {
// Implementation for updating post
return { success: true, postId: input.id };
}),
deletePost: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) => {
// Implementation for deleting post
return { success: true, postId: input.id };
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const fullURL = input.banner_photo
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo
: null;
const query = `
INSERT INTO Post (title, category, subtitle, body, banner_photo, published, author_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
const params = [
input.title,
input.category,
input.subtitle,
input.body,
fullURL,
input.published,
input.author_id,
];
const results = await conn.execute({ sql: query, args: params });
if (input.tags && input.tags.length > 0) {
let tagQuery = "INSERT INTO Tag (value, post_id) VALUES ";
let values = input.tags.map(
(tag) => `("${tag}", ${results.lastInsertRowid})`
);
tagQuery += values.join(", ");
await conn.execute(tagQuery);
}
return { data: results.lastInsertRowid };
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create post",
});
}
}),
// Post likes routes
getPostLikes: publicProcedure
.input(z.object({ postId: z.string() }))
.query(({ input }) => {
// Implementation for getting post likes
return { postId: input.postId, likes: [] };
updatePost: publicProcedure
.input(z.object({
id: z.number(),
title: z.string().nullable().optional(),
subtitle: z.string().nullable().optional(),
body: z.string().nullable().optional(),
banner_photo: z.string().nullable().optional(),
published: z.boolean().nullable().optional(),
tags: z.array(z.string()).nullable().optional(),
author_id: z.string(),
}))
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
let query = "UPDATE Post SET ";
let params: any[] = [];
let first = true;
if (input.title !== undefined && input.title !== null) {
query += first ? "title = ?" : ", title = ?";
params.push(input.title);
first = false;
}
if (input.subtitle !== undefined && input.subtitle !== null) {
query += first ? "subtitle = ?" : ", subtitle = ?";
params.push(input.subtitle);
first = false;
}
if (input.body !== undefined && input.body !== null) {
query += first ? "body = ?" : ", body = ?";
params.push(input.body);
first = false;
}
if (input.banner_photo !== undefined && input.banner_photo !== null) {
query += first ? "banner_photo = ?" : ", banner_photo = ?";
if (input.banner_photo === "_DELETE_IMAGE_") {
params.push(null);
} else {
params.push(env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo);
}
first = false;
}
if (input.published !== undefined && input.published !== null) {
query += first ? "published = ?" : ", published = ?";
params.push(input.published);
first = false;
}
query += first ? "author_id = ?" : ", author_id = ?";
params.push(input.author_id);
query += " WHERE id = ?";
params.push(input.id);
const results = await conn.execute({ sql: query, args: params });
// Handle tags
const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`;
await conn.execute({ sql: deleteTagsQuery, args: [input.id.toString()] });
if (input.tags && input.tags.length > 0) {
let tagQuery = "INSERT INTO Tag (value, post_id) VALUES ";
let values = input.tags.map((tag) => `("${tag}", ${input.id})`);
tagQuery += values.join(", ");
await conn.execute(tagQuery);
}
return { data: results.lastInsertRowid };
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update post",
});
}
}),
likePost: publicProcedure
.input(z.object({ postId: z.string() }))
.mutation(({ input }) => {
// Implementation for liking post
return { success: true, postId: input.postId };
// ============================================================
// Post Likes Routes
// ============================================================
addPostLike: publicProcedure
.input(z.object({
user_id: z.string(),
post_id: z.string(),
}))
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `INSERT INTO PostLike (user_id, post_id) VALUES (?, ?)`;
await conn.execute({
sql: query,
args: [input.user_id, input.post_id],
});
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
const res = await conn.execute({
sql: followUpQuery,
args: [input.post_id],
});
return { newLikes: res.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to add post like",
});
}
}),
unlikePost: publicProcedure
.input(z.object({ postId: z.string() }))
.mutation(({ input }) => {
// Implementation for unliking post
return { success: true, postId: input.postId };
removePostLike: publicProcedure
.input(z.object({
user_id: z.string(),
post_id: z.string(),
}))
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `
DELETE FROM PostLike
WHERE user_id = ? AND post_id = ?
`;
await conn.execute({
sql: query,
args: [input.user_id, input.post_id],
});
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
const res = await conn.execute({
sql: followUpQuery,
args: [input.post_id],
});
return { newLikes: res.rows };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to remove post like",
});
}
}),
});
// ============================================================
// User Routes
// ============================================================
getUserById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = "SELECT * FROM User WHERE id = ?";
const res = await conn.execute({
sql: query,
args: [input.id],
});
if (res.rows[0]) {
const user = res.rows[0] as any;
if (user && user.display_name !== "user deleted") {
return {
id: user.id,
email: user.email,
emailVerified: user.email_verified,
image: user.image,
displayName: user.display_name,
provider: user.provider,
hasPassword: !!user.password_hash,
};
}
}
return null;
} catch (error) {
console.error(error);
return null;
}
}),
getUserPublicData: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = "SELECT email, display_name, image FROM User WHERE id = ?";
const res = await conn.execute({
sql: query,
args: [input.id],
});
if (res.rows[0]) {
const user = res.rows[0] as any;
if (user && user.display_name !== "user deleted") {
return {
email: user.email,
image: user.image,
display_name: user.display_name,
};
}
}
return null;
} catch (error) {
console.error(error);
return null;
}
}),
getUserImage: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = "SELECT * FROM User WHERE id = ?";
const results = await conn.execute({
sql: query,
args: [input.id],
});
return { user: results.rows[0] };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch user image",
});
}
}),
updateUserImage: publicProcedure
.input(z.object({
id: z.string(),
imageURL: z.string(),
}))
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const fullURL = input.imageURL
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.imageURL
: null;
const query = `UPDATE User SET image = ? WHERE id = ?`;
await conn.execute({
sql: query,
args: [fullURL, input.id],
});
return { res: "success" };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update user image",
});
}
}),
updateUserEmail: publicProcedure
.input(z.object({
id: z.string(),
newEmail: z.string().email(),
oldEmail: z.string().email(),
}))
.mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`;
const res = await conn.execute({
sql: query,
args: [input.newEmail, input.id, input.oldEmail],
});
return { res };
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update user email",
});
}
}),
});

View File

@@ -1,11 +1,34 @@
import { wrap } from "@typeschema/valibot";
import { string } from "valibot";
import { createTRPCRouter, publicProcedure } from "../utils";
import {
createTRPCRouter,
publicProcedure,
protectedProcedure,
adminProcedure
} from "../utils";
export const exampleRouter = createTRPCRouter({
hello: publicProcedure
.input(wrap(string()))
.query(({ input }) => {
return `Hello ${input}!`;
})
}),
// Example of a protected procedure (requires authentication)
getProfile: protectedProcedure.query(({ ctx }) => {
return {
userId: ctx.userId,
privilegeLevel: ctx.privilegeLevel,
message: "You are authenticated!",
};
}),
// Example of an admin-only procedure
adminDashboard: adminProcedure.query(({ ctx }) => {
return {
userId: ctx.userId,
message: "Welcome to the admin dashboard!",
isAdmin: true,
};
}),
});

View File

@@ -1,124 +1,27 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
import { createTRPCRouter } from "../utils";
import { lineageAuthRouter } from "./lineage/auth";
import { lineageDatabaseRouter } from "./lineage/database";
import { lineageJsonServiceRouter } from "./lineage/json-service";
import { lineageMiscRouter } from "./lineage/misc";
import { lineagePvpRouter } from "./lineage/pvp";
import { lineageMaintenanceRouter } from "./lineage/maintenance";
export const lineageRouter = createTRPCRouter({
// Database management routes (GET)
databaseManagement: publicProcedure
.query(async () => {
// Implementation for database management
return { message: "Database management endpoint" };
}),
// Authentication
auth: lineageAuthRouter,
// Analytics route (GET)
analytics: publicProcedure
.query(async () => {
// Implementation for analytics
return { message: "Analytics endpoint" };
}),
// Database Management
database: lineageDatabaseRouter,
// Apple authentication routes (GET)
appleAuth: publicProcedure
.query(async () => {
// Implementation for Apple authentication
return { message: "Apple authentication endpoint" };
}),
// PvP
pvp: lineagePvpRouter,
// Email login/registration/verification routes (GET/POST)
emailLogin: publicProcedure
.input(z.object({ email: z.string().email(), password: z.string() }))
.mutation(async ({ input }) => {
// Implementation for email login
return { message: `Email login for ${input.email}` };
}),
// JSON Service
jsonService: lineageJsonServiceRouter,
emailRegister: publicProcedure
.input(z.object({ email: z.string().email(), password: z.string() }))
.mutation(async ({ input }) => {
// Implementation for email registration
return { message: `Email registration for ${input.email}` };
}),
// Misc (Analytics, Tokens, etc.)
misc: lineageMiscRouter,
emailVerify: publicProcedure
.input(z.object({ token: z.string() }))
.mutation(async ({ input }) => {
// Implementation for email verification
return { message: "Email verification endpoint" };
}),
// Google registration route (POST)
googleRegister: publicProcedure
.input(z.object({
googleId: z.string(),
email: z.string().email(),
name: z.string()
}))
.mutation(async ({ input }) => {
// Implementation for Google registration
return { message: `Google registration for ${input.email}` };
}),
// JSON service routes (GET - attacks, conditions, dungeons, enemies, items, misc)
attacks: publicProcedure
.query(async () => {
// Implementation for attacks data
return { message: "Attacks data" };
}),
conditions: publicProcedure
.query(async () => {
// Implementation for conditions data
return { message: "Conditions data" };
}),
dungeons: publicProcedure
.query(async () => {
// Implementation for dungeons data
return { message: "Dungeons data" };
}),
enemies: publicProcedure
.query(async () => {
// Implementation for enemies data
return { message: "Enemies data" };
}),
items: publicProcedure
.query(async () => {
// Implementation for items data
return { message: "Items data" };
}),
misc: publicProcedure
.query(async () => {
// Implementation for miscellaneous data
return { message: "Miscellaneous data" };
}),
// Offline secret route (GET)
offlineSecret: publicProcedure
.query(async () => {
// Implementation for offline secret
return { message: "Offline secret endpoint" };
}),
// PvP routes (GET/POST)
pvpGet: publicProcedure
.query(async () => {
// Implementation for PvP GET
return { message: "PvP GET endpoint" };
}),
pvpPost: publicProcedure
.input(z.object({ player1: z.string(), player2: z.string() }))
.mutation(async ({ input }) => {
// Implementation for PvP POST
return { message: `PvP battle between ${input.player1} and ${input.player2}` };
}),
// Tokens route (GET)
tokens: publicProcedure
.query(async () => {
// Implementation for tokens
return { message: "Tokens endpoint" };
}),
// Maintenance (Protected)
maintenance: lineageMaintenanceRouter,
});

View File

@@ -0,0 +1,493 @@
import { createTRPCRouter, publicProcedure } from "../../utils";
import { z } from "zod";
import {
LineageConnectionFactory,
LineageDBInit,
hashPassword,
checkPassword,
sendEmailVerification,
LINEAGE_JWT_EXPIRY,
} from "~/server/utils";
import { env } from "~/env/server";
import { TRPCError } from "@trpc/server";
import { SignJWT, jwtVerify } from "jose";
import { LibsqlError } from "@libsql/client/web";
import { createClient as createAPIClient } from "@tursodatabase/api";
export const lineageAuthRouter = createTRPCRouter({
emailLogin: publicProcedure
.input(
z.object({
email: z.string().email(),
password: z.string().min(8),
})
)
.mutation(async ({ input }) => {
const { email, password } = input;
const conn = LineageConnectionFactory();
const query = `SELECT * FROM User WHERE email = ? AND provider = ? LIMIT 1`;
const params = [email, "email"];
const res = await conn.execute({ sql: query, args: params });
if (res.rows.length === 0) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid Credentials",
});
}
const user = res.rows[0];
if (user.email_verified === 0) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Email not yet verified!",
});
}
const valid = await checkPassword(password, user.password_hash as string);
if (!valid) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid Credentials",
});
}
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ userId: user.id, email: user.email })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(LINEAGE_JWT_EXPIRY)
.sign(secret);
return {
success: true,
message: "Login successful",
token,
email,
};
}),
emailRegistration: publicProcedure
.input(
z.object({
email: z.string().email(),
password: z.string().min(8),
password_conf: z.string().min(8),
})
)
.mutation(async ({ input }) => {
const { email, password, password_conf } = input;
if (password !== password_conf) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Password mismatch",
});
}
const passwordHash = await hashPassword(password);
const conn = LineageConnectionFactory();
const userCreationQuery = `
INSERT INTO User (email, provider, password_hash)
VALUES (?, ?, ?)
`;
const params = [email, "email", passwordHash];
try {
await conn.execute({ sql: userCreationQuery, args: params });
const emailResult = await sendEmailVerification(email);
if (emailResult.success && emailResult.messageId) {
return {
success: true,
message: "Email verification sent!",
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: emailResult.message || "Failed to send verification email",
});
}
} catch (e) {
console.error(e);
if (e instanceof LibsqlError && e.code === "SQLITE_CONSTRAINT") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "User already exists",
});
}
if (e instanceof TRPCError) throw e;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "An error occurred while creating the user",
});
}
}),
emailVerification: publicProcedure
.input(
z.object({
email: z.string().email(),
token: z.string(),
})
)
.mutation(async ({ input }) => {
const { email: userEmail, token } = input;
let conn;
let dbName;
let dbToken;
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(token, secret);
if (payload.email !== userEmail) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication failed: email mismatch",
});
}
conn = LineageConnectionFactory();
const dbInit = await LineageDBInit();
dbName = dbInit.dbName;
dbToken = dbInit.token;
const query = `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`;
const queryParams = [true, dbName, dbToken, userEmail];
const res = await conn.execute({ sql: query, args: queryParams });
if (res.rowsAffected === 0) {
throw new Error("User not found or update failed");
}
return {
success: true,
message:
"Email verification success. You may close this window and sign in within the app.",
};
} catch (err) {
console.error("Error in email verification:", err);
if (dbName) {
try {
const turso = createAPIClient({
org: "mikefreno",
token: env.TURSO_DB_API_TOKEN,
});
await turso.databases.delete(dbName);
console.log(`Database ${dbName} deleted due to error`);
} catch (deleteErr) {
console.error("Error deleting database:", deleteErr);
}
}
if (conn) {
try {
await conn.execute({
sql: `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`,
args: [false, null, null, userEmail],
});
console.log("User table update reverted");
} catch (revertErr) {
console.error("Error reverting User table update:", revertErr);
}
}
if (err instanceof TRPCError) throw err;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"Authentication failed: An error occurred during email verification. Please try again.",
});
}
}),
refreshVerification: publicProcedure
.input(z.object({ email: z.string().email() }))
.mutation(async ({ input }) => {
const { email } = input;
const conn = LineageConnectionFactory();
const query = "SELECT * FROM User WHERE email = ?";
const params = [email];
const res = await conn.execute({ sql: query, args: params });
if (res.rows.length === 0 || res.rows[0].email_verified) {
throw new TRPCError({
code: "CONFLICT",
message: "Invalid Request",
});
}
const emailResult = await sendEmailVerification(email);
if (emailResult.success && emailResult.messageId) {
return {
success: true,
message: "Email verification sent!",
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: emailResult.message || "Failed to send verification email",
});
}
}),
refreshToken: publicProcedure
.input(z.object({ token: z.string() }))
.query(async ({ input }) => {
const { token } = input;
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(token, secret);
const newToken = await new SignJWT({
userId: payload.userId,
email: payload.email,
})
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(LINEAGE_JWT_EXPIRY)
.sign(secret);
return {
status: 200,
ok: true,
valid: true,
token: newToken,
email: payload.email,
};
} catch (error) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid or expired token",
});
}
}),
googleRegistration: publicProcedure
.input(z.object({ email: z.string().email() }))
.mutation(async ({ input }) => {
const { email } = input;
const conn = LineageConnectionFactory();
try {
const checkUserQuery = "SELECT * FROM User WHERE email = ?";
const checkUserResult = await conn.execute({
sql: checkUserQuery,
args: [email],
});
if (checkUserResult.rows.length > 0) {
const updateQuery = `
UPDATE User
SET provider = ?
WHERE email = ?
`;
const updateRes = await conn.execute({
sql: updateQuery,
args: ["google", email],
});
if (updateRes.rowsAffected !== 0) {
return {
success: true,
message: "User information updated",
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "User update failed!",
});
}
} else {
let db_name;
try {
const { token, dbName } = await LineageDBInit();
db_name = dbName;
console.log("init success");
const insertQuery = `
INSERT INTO User (email, email_verified, provider, database_name, database_token)
VALUES (?, ?, ?, ?, ?)
`;
await conn.execute({
sql: insertQuery,
args: [email, true, "google", dbName, token],
});
console.log("insert success");
return {
success: true,
message: "New user created",
};
} catch (error) {
if (db_name) {
const turso = createAPIClient({
org: "mikefreno",
token: env.TURSO_DB_API_TOKEN,
});
await turso.databases.delete(db_name);
}
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create user",
});
}
}
} catch (error) {
console.error("Error in Google Sign-Up handler:", error);
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "An error occurred while processing the request",
});
}
}),
appleRegistration: publicProcedure
.input(
z.object({
email: z.string().email().optional(),
userString: z.string(),
})
)
.mutation(async ({ input }) => {
const { email, userString } = input;
let dbName;
let dbToken;
const conn = LineageConnectionFactory();
try {
let checkUserQuery = "SELECT * FROM User WHERE apple_user_string = ?";
let args: string[] = [userString];
if (email) {
args.push(email);
checkUserQuery += " OR email = ?";
}
const checkUserResult = await conn.execute({
sql: checkUserQuery,
args: args,
});
if (checkUserResult.rows.length > 0) {
const setClauses = [];
const values = [];
if (email) {
setClauses.push("email = ?");
values.push(email);
}
setClauses.push("provider = ?", "apple_user_string = ?");
values.push("apple", userString);
const whereClause = `WHERE apple_user_string = ?${
email ? " OR email = ?" : ""
}`;
values.push(userString);
if (email) {
values.push(email);
}
const updateQuery = `UPDATE User SET ${setClauses.join(
", "
)} ${whereClause}`;
const updateRes = await conn.execute({
sql: updateQuery,
args: values,
});
if (updateRes.rowsAffected !== 0) {
return {
success: true,
message: "User information updated",
email: checkUserResult.rows[0].email as string,
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "User update failed!",
});
}
} else {
const dbInit = await LineageDBInit();
dbToken = dbInit.token;
dbName = dbInit.dbName;
try {
const insertQuery = `
INSERT INTO User (email, email_verified, apple_user_string, provider, database_name, database_token)
VALUES (?, ?, ?, ?, ?, ?)
`;
await conn.execute({
sql: insertQuery,
args: [email, true, userString, "apple", dbName, dbToken],
});
return {
success: true,
message: "New user created",
dbName,
dbToken,
};
} catch (error) {
if (dbName) {
const turso = createAPIClient({
org: "mikefreno",
token: env.TURSO_DB_API_TOKEN,
});
await turso.databases.delete(dbName);
}
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create user",
});
}
}
} catch (error) {
if (dbName) {
try {
const turso = createAPIClient({
org: "mikefreno",
token: env.TURSO_DB_API_TOKEN,
});
await turso.databases.delete(dbName);
} catch (deleteErr) {
console.error("Error deleting database:", deleteErr);
}
}
console.error("Error in Apple Sign-Up handler:", error);
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "An error occurred while processing the request",
});
}
}),
appleGetEmail: publicProcedure
.input(z.object({ userString: z.string() }))
.mutation(async ({ input }) => {
const { userString } = input;
const conn = LineageConnectionFactory();
const query = "SELECT * FROM User WHERE apple_user_string = ?";
const res = await conn.execute({ sql: query, args: [userString] });
if (res.rows.length > 0) {
return { success: true, email: res.rows[0].email as string };
} else {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
}),
});

View File

@@ -0,0 +1,393 @@
import { createTRPCRouter, publicProcedure } from "../../utils";
import { z } from "zod";
import {
LineageConnectionFactory,
validateLineageRequest,
dumpAndSendDB,
} from "~/server/utils";
import { env } from "~/env/server";
import { TRPCError } from "@trpc/server";
import { OAuth2Client } from "google-auth-library";
import { jwtVerify } from "jose";
export const lineageDatabaseRouter = createTRPCRouter({
credentials: publicProcedure
.input(
z.object({
email: z.string().email(),
provider: z.enum(["email", "google", "apple"]),
authToken: z.string(),
})
)
.mutation(async ({ input }) => {
const { email, provider, authToken } = input;
try {
let valid_request = false;
if (provider === "email") {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(authToken, secret);
if (payload.email === email) {
valid_request = true;
}
} else if (provider === "google") {
const CLIENT_ID = env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE;
if (!CLIENT_ID) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Google client ID not configured",
});
}
const client = new OAuth2Client(CLIENT_ID);
const ticket = await client.verifyIdToken({
idToken: authToken,
audience: CLIENT_ID,
});
if (ticket.getPayload()?.email === email) {
valid_request = true;
}
} else {
const conn = LineageConnectionFactory();
const query = "SELECT * FROM User WHERE apple_user_string = ?";
const res = await conn.execute({ sql: query, args: [authToken] });
if (res.rows.length > 0 && res.rows[0].email === email) {
valid_request = true;
}
}
if (valid_request) {
const conn = LineageConnectionFactory();
const query = "SELECT * FROM User WHERE email = ? LIMIT 1";
const params = [email];
const res = await conn.execute({ sql: query, args: params });
if (res.rows.length === 1) {
const user = res.rows[0];
return {
success: true,
db_name: user.database_name as string,
db_token: user.database_token as string,
};
}
throw new TRPCError({
code: "NOT_FOUND",
message: "No user found",
});
} else {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials",
});
}
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication failed",
});
}
}),
deletionInit: publicProcedure
.input(
z.object({
email: z.string().email(),
db_name: z.string(),
db_token: z.string(),
authToken: z.string(),
skip_cron: z.boolean().optional(),
send_dump_target: z.string().email().optional(),
})
)
.mutation(async ({ input }) => {
const { email, db_name, db_token, authToken, skip_cron, send_dump_target } = input;
const conn = LineageConnectionFactory();
const res = await conn.execute({
sql: `SELECT * FROM User WHERE email = ?`,
args: [email],
});
const userRow = res.rows[0];
if (!userRow) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const valid = await validateLineageRequest({
auth_token: authToken,
userRow,
});
if (!valid) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid Verification",
});
}
const { database_token, database_name } = userRow;
if (database_token !== db_token || database_name !== db_name) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Incorrect Verification",
});
}
if (skip_cron) {
if (send_dump_target) {
const dumpRes = await dumpAndSendDB({
dbName: db_name,
dbToken: db_token,
sendTarget: send_dump_target,
});
if (dumpRes.success) {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
},
}
);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email],
});
return {
ok: true,
status: 200,
message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`,
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database",
});
}
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: dumpRes.reason || "Failed to dump database",
});
}
} else {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
},
}
);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email],
});
return {
ok: true,
status: 200,
message: `Account and Database deleted`,
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database",
});
}
}
} else {
const insertRes = await conn.execute({
sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`,
args: [email, db_name, db_token, send_dump_target],
});
if (insertRes.rowsAffected > 0) {
return {
ok: true,
status: 200,
message: `Deletion scheduled.`,
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Deletion not scheduled, due to server failure`,
});
}
}
}),
deletionCheck: publicProcedure
.input(z.object({ email: z.string().email() }))
.mutation(async ({ input }) => {
const { email } = input;
const conn = LineageConnectionFactory();
try {
const res = await conn.execute({
sql: `SELECT * FROM cron WHERE email = ?`,
args: [email],
});
const cronRow = res.rows[0];
if (!cronRow) {
return { status: 204, ok: true };
}
return {
ok: true,
status: 200,
created_at: cronRow.created_at as string,
};
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to check deletion status",
});
}
}),
deletionCancel: publicProcedure
.input(
z.object({
email: z.string().email(),
authToken: z.string(),
})
)
.mutation(async ({ input }) => {
const { email, authToken } = input;
const conn = LineageConnectionFactory();
const resUser = await conn.execute({
sql: `SELECT * FROM User WHERE email = ?;`,
args: [email],
});
if (resUser.rows.length === 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found.",
});
}
const userRow = resUser.rows[0];
const valid = await validateLineageRequest({
auth_token: authToken,
userRow,
});
if (!valid) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials for cancelation.",
});
}
const result = await conn.execute({
sql: `DELETE FROM cron WHERE email = ?;`,
args: [email],
});
if (result.rowsAffected > 0) {
return {
status: 200,
ok: true,
message: "Cron job(s) canceled successfully.",
};
} else {
throw new TRPCError({
code: "NOT_FOUND",
message: "No cron job found for the given email.",
});
}
}),
deletionCron: publicProcedure.query(async () => {
const conn = LineageConnectionFactory();
const res = await conn.execute(
`SELECT * FROM cron WHERE created_at <= datetime('now', '-1 day');`
);
if (res.rows.length > 0) {
const executed_ids: (number | string)[] = [];
for (const row of res.rows) {
const { id, db_name, db_token, send_dump_target, email } = row;
if (send_dump_target) {
const dumpRes = await dumpAndSendDB({
dbName: db_name as string,
dbToken: db_token as string,
sendTarget: send_dump_target as string,
});
if (dumpRes.success) {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
},
}
);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email],
});
executed_ids.push(id as number);
}
}
} else {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
},
}
);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email],
});
executed_ids.push(id as number);
}
}
}
if (executed_ids.length > 0) {
const placeholders = executed_ids.map(() => "?").join(", ");
const deleteQuery = `DELETE FROM cron WHERE id IN (${placeholders});`;
await conn.execute({ sql: deleteQuery, args: executed_ids });
return {
status: 200,
message:
"Processed databases deleted and corresponding cron rows removed.",
};
}
}
return { status: 200, ok: true };
}),
});

View File

@@ -0,0 +1,141 @@
import { createTRPCRouter, publicProcedure } from "../../utils";
// Attack data imports
import playerAttacks from "~/lineage-json/attack-route/playerAttacks.json";
import mageBooks from "~/lineage-json/attack-route/mageBooks.json";
import mageSpells from "~/lineage-json/attack-route/mageSpells.json";
import necroBooks from "~/lineage-json/attack-route/necroBooks.json";
import necroSpells from "~/lineage-json/attack-route/necroSpells.json";
import rangerBooks from "~/lineage-json/attack-route/rangerBooks.json";
import rangerSpells from "~/lineage-json/attack-route/rangerSpells.json";
import paladinBooks from "~/lineage-json/attack-route/paladinBooks.json";
import paladinSpells from "~/lineage-json/attack-route/paladinSpells.json";
import summons from "~/lineage-json/attack-route/summons.json";
// Conditions data imports
import conditions from "~/lineage-json/conditions-route/conditions.json";
import debilitations from "~/lineage-json/conditions-route/debilitations.json";
import sanityDebuffs from "~/lineage-json/conditions-route/sanityDebuffs.json";
// Dungeon data imports
import dungeons from "~/lineage-json/dungeon-route/dungeons.json";
import specialEncounters from "~/lineage-json/dungeon-route/specialEncounters.json";
// Enemy data imports
import bosses from "~/lineage-json/enemy-route/bosses.json";
import enemies from "~/lineage-json/enemy-route/enemy.json";
import enemyAttacks from "~/lineage-json/enemy-route/enemyAttacks.json";
// Item data imports
import arrows from "~/lineage-json/item-route/arrows.json";
import bows from "~/lineage-json/item-route/bows.json";
import foci from "~/lineage-json/item-route/foci.json";
import hats from "~/lineage-json/item-route/hats.json";
import junk from "~/lineage-json/item-route/junk.json";
import melee from "~/lineage-json/item-route/melee.json";
import robes from "~/lineage-json/item-route/robes.json";
import wands from "~/lineage-json/item-route/wands.json";
import ingredients from "~/lineage-json/item-route/ingredients.json";
import storyItems from "~/lineage-json/item-route/storyItems.json";
import artifacts from "~/lineage-json/item-route/artifacts.json";
import shields from "~/lineage-json/item-route/shields.json";
import bodyArmor from "~/lineage-json/item-route/bodyArmor.json";
import helmets from "~/lineage-json/item-route/helmets.json";
import suffix from "~/lineage-json/item-route/suffix.json";
import prefix from "~/lineage-json/item-route/prefix.json";
import potions from "~/lineage-json/item-route/potions.json";
import poison from "~/lineage-json/item-route/poison.json";
import staves from "~/lineage-json/item-route/staves.json";
// Misc data imports
import activities from "~/lineage-json/misc-route/activities.json";
import investments from "~/lineage-json/misc-route/investments.json";
import jobs from "~/lineage-json/misc-route/jobs.json";
import manaOptions from "~/lineage-json/misc-route/manaOptions.json";
import otherOptions from "~/lineage-json/misc-route/otherOptions.json";
import healthOptions from "~/lineage-json/misc-route/healthOptions.json";
import sanityOptions from "~/lineage-json/misc-route/sanityOptions.json";
import pvpRewards from "~/lineage-json/misc-route/pvpRewards.json";
export const lineageJsonServiceRouter = createTRPCRouter({
attacks: publicProcedure.query(() => {
return {
ok: true,
playerAttacks,
mageBooks,
mageSpells,
necroBooks,
necroSpells,
rangerBooks,
rangerSpells,
paladinBooks,
paladinSpells,
summons,
};
}),
conditions: publicProcedure.query(() => {
return {
ok: true,
conditions,
debilitations,
sanityDebuffs,
};
}),
dungeons: publicProcedure.query(() => {
return {
ok: true,
dungeons,
specialEncounters,
};
}),
enemies: publicProcedure.query(() => {
return {
ok: true,
bosses,
enemies,
enemyAttacks,
};
}),
items: publicProcedure.query(() => {
return {
ok: true,
arrows,
bows,
foci,
hats,
junk,
melee,
robes,
wands,
ingredients,
storyItems,
artifacts,
shields,
bodyArmor,
helmets,
suffix,
prefix,
potions,
poison,
staves,
};
}),
misc: publicProcedure.query(() => {
return {
ok: true,
activities,
investments,
jobs,
manaOptions,
otherOptions,
healthOptions,
sanityOptions,
pvpRewards,
};
}),
});

View File

@@ -0,0 +1,83 @@
import { createTRPCRouter, adminProcedure } from "../../utils";
import { LineageConnectionFactory } from "~/server/utils";
import { env } from "~/env/server";
import { TRPCError } from "@trpc/server";
import { createClient as createAPIClient } from "@tursodatabase/api";
const IGNORE = ["frenome", "magic-delve-conductor"];
export const lineageMaintenanceRouter = createTRPCRouter({
findLooseDatabases: adminProcedure.query(async () => {
const conn = LineageConnectionFactory();
const query = "SELECT database_url FROM User WHERE database_url IS NOT NULL";
try {
const res = await conn.execute(query);
const turso = createAPIClient({
org: "mikefreno",
token: env.TURSO_DB_API_TOKEN,
});
const linkedDatabaseUrls = res.rows.map((row) => row.database_url);
const all_dbs = await turso.databases.list();
const dbs_to_delete = all_dbs.filter((db) => {
return !IGNORE.includes(db.name) && !linkedDatabaseUrls.includes(db.name);
});
return {
success: true,
looseDatabases: dbs_to_delete,
count: dbs_to_delete.length,
};
} catch (e) {
console.error("Error finding loose databases:", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to find loose databases",
});
}
}),
cleanupExpiredDatabases: adminProcedure.query(async () => {
const conn = LineageConnectionFactory();
const query =
"SELECT * FROM User WHERE datetime(db_destroy_date) < datetime('now');";
try {
const res = await conn.execute(query);
const turso = createAPIClient({
org: "mikefreno",
token: env.TURSO_DB_API_TOKEN,
});
const deletedDatabases = [];
for (const row of res.rows) {
const db_url = row.database_url;
try {
await turso.databases.delete(db_url as string);
const updateQuery =
"UPDATE User SET database_url = ?, database_token = ?, db_destroy_date = ? WHERE id = ?";
const params = [null, null, null, row.id];
await conn.execute({ sql: updateQuery, args: params });
deletedDatabases.push(db_url);
} catch (deleteErr) {
console.error(`Failed to delete database ${db_url}:`, deleteErr);
}
}
return {
success: true,
deletedDatabases,
count: deletedDatabases.length,
};
} catch (e) {
console.error("Error cleaning up expired databases:", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to cleanup expired databases",
});
}
}),
});

View File

@@ -0,0 +1,95 @@
import { createTRPCRouter, publicProcedure } from "../../utils";
import { z } from "zod";
import { LineageConnectionFactory } from "~/server/utils";
import { env } from "~/env/server";
import { TRPCError } from "@trpc/server";
export const lineageMiscRouter = createTRPCRouter({
analytics: publicProcedure
.input(
z.object({
playerID: z.string(),
dungeonProgression: z.record(z.unknown()),
playerClass: z.string(),
spellCount: z.number(),
proficiencies: z.record(z.unknown()),
jobs: z.record(z.unknown()),
resistanceTable: z.record(z.unknown()),
damageTable: z.record(z.unknown()),
})
)
.mutation(async ({ input }) => {
const {
playerID,
dungeonProgression,
playerClass,
spellCount,
proficiencies,
jobs,
resistanceTable,
damageTable,
} = input;
const conn = LineageConnectionFactory();
try {
const res = await conn.execute({
sql: `
INSERT OR REPLACE INTO Analytics
(playerID, dungeonProgression, playerClass, spellCount, proficiencies, jobs, resistanceTable, damageTable)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
args: [
playerID,
JSON.stringify(dungeonProgression),
playerClass,
spellCount,
JSON.stringify(proficiencies),
JSON.stringify(jobs),
JSON.stringify(resistanceTable),
JSON.stringify(damageTable),
],
});
return { success: true, status: 200 };
} catch (e) {
console.error("Analytics error:", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to store analytics",
});
}
}),
tokens: publicProcedure
.input(z.object({ token: z.string() }))
.mutation(async ({ input }) => {
const { token } = input;
if (!token) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Missing token in body",
});
}
const conn = LineageConnectionFactory();
const query = "SELECT * FROM Token WHERE token = ?";
const res = await conn.execute({ sql: query, args: [token] });
if (res.rows.length > 0) {
const queryUpdate =
"UPDATE Token SET last_updated_at = datetime('now') WHERE token = ?";
const resUpdate = await conn.execute({ sql: queryUpdate, args: [token] });
return { success: true, action: "updated", result: resUpdate };
} else {
const queryInsert = "INSERT INTO Token (token) VALUES (?)";
const resInsert = await conn.execute({ sql: queryInsert, args: [token] });
return { success: true, action: "inserted", result: resInsert };
}
}),
offlineSecret: publicProcedure.query(() => {
return { secret: env.LINEAGE_OFFLINE_SERIALIZATION_SECRET };
}),
});

View File

@@ -0,0 +1,229 @@
import { createTRPCRouter, publicProcedure } from "../../utils";
import { z } from "zod";
import { LineageConnectionFactory } from "~/server/utils";
import { TRPCError } from "@trpc/server";
const characterSchema = z.object({
playerClass: z.string(),
blessing: z.string().optional(),
name: z.string(),
maxHealth: z.number(),
maxSanity: z.number(),
maxMana: z.number(),
baseManaRegen: z.number(),
strength: z.number(),
intelligence: z.number(),
dexterity: z.number(),
resistanceTable: z.string(),
damageTable: z.string(),
attackStrings: z.string(),
knownSpells: z.string(),
});
export const lineagePvpRouter = createTRPCRouter({
registerCharacter: publicProcedure
.input(
z.object({
character: characterSchema,
linkID: z.string(),
pushToken: z.string().optional(),
pushCurrentlyEnabled: z.boolean().optional(),
})
)
.mutation(async ({ input }) => {
const { character, linkID, pushToken, pushCurrentlyEnabled } = input;
try {
const conn = LineageConnectionFactory();
const res = await conn.execute({
sql: `SELECT * FROM PvP_Characters WHERE linkID = ?`,
args: [linkID],
});
if (res.rows.length === 0) {
await conn.execute({
sql: `INSERT INTO PvP_Characters (
linkID,
blessing,
playerClass,
name,
maxHealth,
maxSanity,
maxMana,
baseManaRegen,
strength,
intelligence,
dexterity,
resistanceTable,
damageTable,
attackStrings,
knownSpells,
pushToken,
pushCurrentlyEnabled
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
linkID,
character.blessing,
character.playerClass,
character.name,
character.maxHealth,
character.maxSanity,
character.maxMana,
character.baseManaRegen,
character.strength,
character.intelligence,
character.dexterity,
character.resistanceTable,
character.damageTable,
character.attackStrings,
character.knownSpells,
pushToken,
pushCurrentlyEnabled,
],
});
return {
ok: true,
winCount: 0,
lossCount: 0,
tokenRedemptionCount: 0,
status: 201,
};
} else {
await conn.execute({
sql: `UPDATE PvP_Characters SET
playerClass = ?,
blessing = ?,
name = ?,
maxHealth = ?,
maxSanity = ?,
maxMana = ?,
baseManaRegen = ?,
strength = ?,
intelligence = ?,
dexterity = ?,
resistanceTable = ?,
damageTable = ?,
attackStrings = ?,
knownSpells = ?,
pushToken = ?,
pushCurrentlyEnabled = ?
WHERE linkID = ?`,
args: [
character.playerClass,
character.blessing,
character.name,
character.maxHealth,
character.maxSanity,
character.maxMana,
character.baseManaRegen,
character.strength,
character.intelligence,
character.dexterity,
character.resistanceTable,
character.damageTable,
character.attackStrings,
character.knownSpells,
pushToken,
pushCurrentlyEnabled,
linkID,
],
});
return {
ok: true,
winCount: res.rows[0].winCount as number,
lossCount: res.rows[0].lossCount as number,
tokenRedemptionCount: res.rows[0].tokenRedemptionCount as number,
status: 200,
};
}
} catch (e) {
console.error(e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to register character",
});
}
}),
getOpponents: publicProcedure.query(async () => {
const conn = LineageConnectionFactory();
try {
const res = await conn.execute(
`
SELECT playerClass,
blessing,
name,
maxHealth,
maxSanity,
maxMana,
baseManaRegen,
strength,
intelligence,
dexterity,
resistanceTable,
damageTable,
attackStrings,
knownSpells,
linkID,
winCount,
lossCount
FROM PvP_Characters
ORDER BY RANDOM()
LIMIT 3
`
);
return {
ok: true,
characters: res.rows,
status: 200,
};
} catch (e) {
console.error(e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get opponents",
});
}
}),
battleResult: publicProcedure
.input(
z.object({
winnerLinkID: z.string(),
loserLinkID: z.string(),
})
)
.mutation(async ({ input }) => {
const { winnerLinkID, loserLinkID } = input;
const conn = LineageConnectionFactory();
try {
await conn.execute({
sql: `
UPDATE PvP_Characters
SET
winCount = winCount + CASE WHEN linkID = ? THEN 1 ELSE 0 END,
lossCount = lossCount + CASE WHEN linkID = ? THEN 1 ELSE 0 END
WHERE linkID IN (?, ?)
`,
args: [winnerLinkID, loserLinkID, winnerLinkID, loserLinkID],
});
return {
ok: true,
status: 200,
};
} catch (e) {
console.error(e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to record battle result",
});
}
}),
});

View File

@@ -1,34 +1,204 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { env } from "~/env/server";
import { TRPCError } from "@trpc/server";
import { ConnectionFactory } from "~/server/utils";
import * as bcrypt from "bcrypt";
const assets: Record<string, string> = {
"shapes-with-abigail": "shapes-with-abigail.apk",
"magic-delve": "magic-delve.apk",
cork: "Cork.zip",
};
export const miscRouter = createTRPCRouter({
// Downloads endpoint (GET)
downloads: publicProcedure
.query(async () => {
// Implementation for downloads logic would go here
return { message: "Downloads endpoint" };
}),
// S3 operations (DELETE/GET)
s3Delete: publicProcedure
.input(z.object({ key: z.string() }))
.mutation(async ({ input }) => {
// Implementation for S3 delete logic would go here
return { message: `Deleted S3 object with key: ${input.key}` };
}),
s3Get: publicProcedure
.input(z.object({ key: z.string() }))
// ============================================================
// Downloads endpoint
// ============================================================
getDownloadUrl: publicProcedure
.input(z.object({ asset_name: z.string() }))
.query(async ({ input }) => {
// Implementation for S3 get logic would go here
return { message: `Retrieved S3 object with key: ${input.key}` };
const bucket = "frenomedownloads";
const params = {
Bucket: bucket,
Key: assets[input.asset_name],
};
if (!assets[input.asset_name]) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Asset not found",
});
}
const credentials = {
accessKeyId: env._AWS_ACCESS_KEY,
secretAccessKey: env._AWS_SECRET_KEY,
};
try {
const client = new S3Client({
region: env.AWS_REGION,
credentials: credentials,
});
const command = new GetObjectCommand(params);
const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 });
return { downloadURL: signedUrl };
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to generate download URL",
});
}
}),
// Password hashing endpoint (POST)
hashPassword: publicProcedure
.input(z.object({ password: z.string() }))
// ============================================================
// S3 Operations
// ============================================================
getPreSignedURL: publicProcedure
.input(z.object({
type: z.string(),
title: z.string(),
filename: z.string(),
}))
.mutation(async ({ input }) => {
// Implementation for password hashing logic would go here
return { message: "Password hashed successfully" };
const credentials = {
accessKeyId: env._AWS_ACCESS_KEY,
secretAccessKey: env._AWS_SECRET_KEY,
};
try {
const client = new S3Client({
region: env.AWS_REGION,
credentials: credentials,
});
const Key = `${input.type}/${input.title}/${input.filename}`;
const ext = /^.+\.([^.]+)$/.exec(input.filename);
const s3params = {
Bucket: env.AWS_S3_BUCKET_NAME,
Key,
ContentType: `image/${ext![1]}`,
};
const command = new PutObjectCommand(s3params);
const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 });
return { uploadURL: signedUrl, key: Key };
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to generate pre-signed URL",
});
}
}),
});
deleteImage: publicProcedure
.input(z.object({
key: z.string(),
newAttachmentString: z.string(),
type: z.string(),
id: z.number(),
}))
.mutation(async ({ input }) => {
try {
const s3params = {
Bucket: env.AWS_S3_BUCKET_NAME,
Key: input.key,
};
const client = new S3Client({
region: env.AWS_REGION,
});
const command = new DeleteObjectCommand(s3params);
const res = await client.send(command);
const conn = ConnectionFactory();
const query = `UPDATE ${input.type} SET attachments = ? WHERE id = ?`;
await conn.execute({
sql: query,
args: [input.newAttachmentString, input.id],
});
return res;
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete image",
});
}
}),
simpleDeleteImage: publicProcedure
.input(z.object({ key: z.string() }))
.mutation(async ({ input }) => {
try {
const s3params = {
Bucket: env.AWS_S3_BUCKET_NAME,
Key: input.key,
};
const client = new S3Client({
region: env.AWS_REGION,
});
const command = new DeleteObjectCommand(s3params);
const res = await client.send(command);
return res;
} catch (error) {
console.error(error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete image",
});
}
}),
// ============================================================
// Password Hashing
// ============================================================
hashPassword: publicProcedure
.input(z.object({ password: z.string().min(8) }))
.mutation(async ({ input }) => {
try {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
const hashedPassword = await bcrypt.hash(input.password, salt);
return { hashedPassword };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to hash password",
});
}
}),
checkPassword: publicProcedure
.input(z.object({
password: z.string(),
hash: z.string(),
}))
.mutation(async ({ input }) => {
try {
const match = await bcrypt.compare(input.password, input.hash);
return { match };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to check password",
});
}
}),
});

View File

@@ -1,6 +1,85 @@
import { initTRPC } from "@trpc/server";
import { initTRPC, TRPCError } from "@trpc/server";
import type { APIEvent } from "@solidjs/start/server";
import { getCookie, setCookie } from "vinxi/http";
import { jwtVerify, type JWTPayload } from "jose";
import { env } from "~/env/server";
export const t = initTRPC.create();
export type Context = {
event: APIEvent;
userId: string | null;
privilegeLevel: "anonymous" | "user" | "admin";
};
async function createContextInner(event: APIEvent): Promise<Context> {
const userIDToken = getCookie(event.nativeEvent, "userIDToken");
let userId: string | null = null;
let privilegeLevel: "anonymous" | "user" | "admin" = "anonymous";
if (userIDToken) {
try {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(userIDToken, secret);
if (payload.id && typeof payload.id === "string") {
userId = payload.id;
privilegeLevel = payload.id === env.ADMIN_ID ? "admin" : "user";
}
} catch (err) {
console.log("Failed to authenticate token:", err);
// Clear invalid token
setCookie(event.nativeEvent, "userIDToken", "", {
maxAge: 0,
expires: new Date("2016-10-05"),
});
}
}
return {
event,
userId,
privilegeLevel,
};
}
export const createTRPCContext = (event: APIEvent) => {
return createContextInner(event);
};
export const t = initTRPC.context<Context>().create();
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
// Middleware to enforce authentication
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId || ctx.privilegeLevel === "anonymous") {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
}
return next({
ctx: {
...ctx,
userId: ctx.userId, // userId is non-null here
},
});
});
// Middleware to enforce admin access
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
if (ctx.privilegeLevel !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin access required"
});
}
return next({
ctx: {
...ctx,
userId: ctx.userId!, // userId is non-null for admins
},
});
});
// Protected procedures
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
export const adminProcedure = t.procedure.use(enforceUserIsAdmin);

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" };
}
}