This commit is contained in:
Michael Freno
2025-12-16 22:42:05 -05:00
commit 8fb748f401
81 changed files with 4378 additions and 0 deletions

16
src/server/api/root.ts Normal file
View File

@@ -0,0 +1,16 @@
import { exampleRouter } from "./routers/example";
import { authRouter } from "./routers/auth";
import { databaseRouter } from "./routers/database";
import { lineageRouter } from "./routers/lineage";
import { miscRouter } from "./routers/misc";
import { createTRPCRouter } from "./utils";
export const appRouter = createTRPCRouter({
example: exampleRouter,
auth: authRouter,
database: databaseRouter,
lineage: lineageRouter,
misc: miscRouter
});
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,34 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
export const authRouter = createTRPCRouter({
// GitHub callback route
githubCallback: publicProcedure
.query(async () => {
// Implementation for GitHub OAuth callback
return { message: "GitHub callback endpoint" };
}),
// Google callback route
googleCallback: publicProcedure
.query(async () => {
// Implementation for Google OAuth callback
return { message: "Google callback endpoint" };
}),
// 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}` };
}),
// 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}` };
}),
});

View File

@@ -0,0 +1,101 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
export const databaseRouter = createTRPCRouter({
// 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 };
}),
// Comments routes
getComments: publicProcedure
.input(z.object({ postId: z.string() }))
.query(({ input }) => {
// Implementation for getting comments
return { postId: input.postId, comments: [] };
}),
// Post manipulation routes
getPosts: publicProcedure
.input(z.object({
limit: z.number().optional(),
offset: z.number().optional()
}))
.query(({ input }) => {
// Implementation for getting posts
return { posts: [], total: 0 };
}),
createPost: publicProcedure
.input(z.object({
title: z.string(),
content: 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 };
}),
// Post likes routes
getPostLikes: publicProcedure
.input(z.object({ postId: z.string() }))
.query(({ input }) => {
// Implementation for getting post likes
return { postId: input.postId, likes: [] };
}),
likePost: publicProcedure
.input(z.object({ postId: z.string() }))
.mutation(({ input }) => {
// Implementation for liking post
return { success: true, postId: input.postId };
}),
unlikePost: publicProcedure
.input(z.object({ postId: z.string() }))
.mutation(({ input }) => {
// Implementation for unliking post
return { success: true, postId: input.postId };
}),
});

View File

@@ -0,0 +1,11 @@
import { wrap } from "@typeschema/valibot";
import { string } from "valibot";
import { createTRPCRouter, publicProcedure } from "../utils";
export const exampleRouter = createTRPCRouter({
hello: publicProcedure
.input(wrap(string()))
.query(({ input }) => {
return `Hello ${input}!`;
})
});

View File

@@ -0,0 +1,124 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
export const lineageRouter = createTRPCRouter({
// Database management routes (GET)
databaseManagement: publicProcedure
.query(async () => {
// Implementation for database management
return { message: "Database management endpoint" };
}),
// Analytics route (GET)
analytics: publicProcedure
.query(async () => {
// Implementation for analytics
return { message: "Analytics endpoint" };
}),
// Apple authentication routes (GET)
appleAuth: publicProcedure
.query(async () => {
// Implementation for Apple authentication
return { message: "Apple authentication endpoint" };
}),
// 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}` };
}),
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}` };
}),
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" };
}),
});

View File

@@ -0,0 +1,34 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod";
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() }))
.query(async ({ input }) => {
// Implementation for S3 get logic would go here
return { message: `Retrieved S3 object with key: ${input.key}` };
}),
// Password hashing endpoint (POST)
hashPassword: publicProcedure
.input(z.object({ password: z.string() }))
.mutation(async ({ input }) => {
// Implementation for password hashing logic would go here
return { message: "Password hashed successfully" };
}),
});

6
src/server/api/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { initTRPC } from "@trpc/server";
export const t = initTRPC.create();
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

249
src/server/utils.ts Normal file
View File

@@ -0,0 +1,249 @@
import jwt, { JwtPayload } from "jsonwebtoken";
import { cookies } from "next/headers";
export const LINEAGE_JWT_EXPIRY = "14d";
export async function getPrivilegeLevel(): Promise<
"anonymous" | "admin" | "user"
> {
try {
const userIDToken = (await cookies()).get("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);
}
},
);
});
if (decoded) {
return decoded.id === env.ADMIN_ID ? "admin" : "user";
}
}
} catch (e) {
return "anonymous";
}
return "anonymous";
}
export async function getUserID(): Promise<string | null> {
try {
const userIDToken = (await cookies()).get("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);
}
},
);
});
if (decoded) {
return decoded.id;
}
}
} catch (e) {
return null;
}
return null;
}
import { createClient, Row } from "@libsql/client/web";
import { env } from "@/env.mjs";
// Turso
export function ConnectionFactory() {
const config = {
url: env.TURSO_DB_URL,
authToken: env.TURSO_DB_TOKEN,
};
const conn = createClient(config);
return conn;
}
export function LineageConnectionFactory() {
const config = {
url: env.TURSO_LINEAGE_URL,
authToken: env.TURSO_LINEAGE_TOKEN,
};
const conn = createClient(config);
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",
token: env.TURSO_DB_API_TOKEN,
});
const db_name = uuid();
const db = await turso.databases.create(db_name, { group: "default" });
const token = await turso.databases.createToken(db_name, {
authorization: "full-access",
});
const conn = PerUserDBConnectionFactory(db.name, token.jwt);
await conn.execute(`
CREATE TABLE checkpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
last_updated TEXT NOT NULL,
player_age INTEGER NOT NULL,
player_data TEXT,
time_data TEXT,
dungeon_data TEXT,
character_data TEXT,
shops_data TEXT
)
`);
return { token: token.jwt, dbName: db.name };
}
export function PerUserDBConnectionFactory(dbName: string, token: string) {
const config = {
url: `libsql://${dbName}-mikefreno.turso.io`,
authToken: token,
};
const conn = createClient(config);
return conn;
}
export async function dumpAndSendDB({
dbName,
dbToken,
sendTarget,
}: {
dbName: string;
dbToken: string;
sendTarget: string;
}): Promise<{
success: boolean;
reason?: string;
}> {
const res = await fetch(`https://${dbName}-mikefreno.turso.io/dump`, {
method: "GET",
headers: {
Authorization: `Bearer ${dbToken}`,
},
});
if (!res.ok) {
console.error(res);
return { success: false, reason: "bad dump request response" };
}
const text = await res.text();
const base64Content = Buffer.from(text, "utf-8").toString("base64");
const apiKey = env.SENDINBLUE_KEY as string;
const apiUrl = "https://api.brevo.com/v3/smtp/email";
const emailPayload = {
sender: {
name: "no_reply@freno.me",
email: "no_reply@freno.me",
},
to: [
{
email: sendTarget,
},
],
subject: "Your Lineage Database Dump",
htmlContent:
"<html><body><p>Please find the attached database dump. This contains the state of your person remote Lineage remote saves. Should you ever return to Lineage, you can upload this file to reinstate the saves you had.</p></body></html>",
attachment: [
{
content: base64Content,
name: "database_dump.txt",
},
],
};
const sendRes = await fetch(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json",
},
body: JSON.stringify(emailPayload),
});
if (!sendRes.ok) {
return { success: false, reason: "email send failure" };
} else {
return { success: true };
}
}
export async function validateLineageRequest({
auth_token,
userRow,
}: {
auth_token: string;
userRow: Row;
}): 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) {
return false;
}
} else if (provider == "apple") {
const { apple_user_string } = userRow;
if (apple_user_string !== auth_token) {
return false;
}
} else if (provider == "google") {
const CLIENT_ID = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE;
const client = new OAuth2Client(CLIENT_ID);
const ticket = await client.verifyIdToken({
idToken: auth_token,
audience: CLIENT_ID,
});
if (ticket.getPayload()?.email !== email) {
return false;
}
} else {
return false;
}
return true;
}