init
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
dist
|
||||||
|
.wrangler
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.vinxi
|
||||||
|
app.config.timestamp_*.js
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
gitignore
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# SolidStart
|
||||||
|
|
||||||
|
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm init solid@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm init solid@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
||||||
|
|
||||||
|
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
||||||
|
|
||||||
|
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
|
||||||
11
app.config.ts
Normal file
11
app.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
preset: "vercel",
|
||||||
|
},
|
||||||
|
});
|
||||||
258
docs/trpc-implementation.md
Normal file
258
docs/trpc-implementation.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# tRPC Implementation Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project implements a [tRPC](https://trpc.io/) API layer to provide type-safe communication between the frontend and backend. The implementation follows SolidStart's server-side rendering architecture with a clear separation of concerns.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The tRPC setup is organized in the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── server/
|
||||||
|
│ └── api/
|
||||||
|
│ ├── root.ts # Main router that combines all sub-routers
|
||||||
|
│ ├── utils.ts # tRPC utility functions and initialization
|
||||||
|
│ └── routers/ # Individual API route groups
|
||||||
|
│ ├── auth.ts # Authentication procedures
|
||||||
|
│ ├── database.ts # Database operations
|
||||||
|
│ ├── example.ts # Example procedures
|
||||||
|
│ ├── lineage.ts # Lineage-related APIs
|
||||||
|
│ └── misc.ts # Miscellaneous endpoints
|
||||||
|
└── routes/
|
||||||
|
└── api/
|
||||||
|
└── trpc/
|
||||||
|
└── [trpc].ts # API endpoint handler
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Use tRPC Procedures from the Frontend
|
||||||
|
|
||||||
|
The `api` client is pre-configured and available for use in components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { api } from "~/lib/api";
|
||||||
|
|
||||||
|
// Example usage in a component
|
||||||
|
export function MyComponent() {
|
||||||
|
const [result, setResult] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
try {
|
||||||
|
// Call a tRPC procedure
|
||||||
|
const data = await api.example.hello.query("World");
|
||||||
|
setResult(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error calling tRPC procedure:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{result}</p>
|
||||||
|
<button onClick={handleClick}>Call API</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Route Structure
|
||||||
|
|
||||||
|
### Root Router (`src/server/api/root.ts`)
|
||||||
|
|
||||||
|
The main router combines all individual routers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const appRouter = createTRPCRouter({
|
||||||
|
example: exampleRouter,
|
||||||
|
auth: authRouter,
|
||||||
|
database: databaseRouter,
|
||||||
|
lineage: lineageRouter,
|
||||||
|
misc: miscRouter
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Procedure Types
|
||||||
|
|
||||||
|
tRPC provides two main procedure types:
|
||||||
|
- **Query**: For read-only operations (GET requests)
|
||||||
|
- **Mutation**: For write operations (POST, PUT, DELETE requests)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Query procedure - read-only
|
||||||
|
publicProcedure
|
||||||
|
.input(z.string())
|
||||||
|
.query(({ input }) => {
|
||||||
|
return `Hello ${input}!`;
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mutation procedure - write operation
|
||||||
|
publicProcedure
|
||||||
|
.input(z.object({ name: z.string() }))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
// Logic for creating/updating data
|
||||||
|
return { success: true, name: input.name };
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Endpoints
|
||||||
|
|
||||||
|
### 1. Create a new router file
|
||||||
|
|
||||||
|
Create a new file in `src/server/api/routers/`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const myRouter = createTRPCRouter({
|
||||||
|
// Add your procedures here
|
||||||
|
hello: publicProcedure
|
||||||
|
.input(z.string())
|
||||||
|
.query(({ input }) => {
|
||||||
|
return `Hello ${input}!`;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Register the router in the root
|
||||||
|
|
||||||
|
Add your new router to `src/server/api/root.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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 { myRouter } from "./routers/myRouter"; // Add this import
|
||||||
|
import { createTRPCRouter } from "./utils";
|
||||||
|
|
||||||
|
export const appRouter = createTRPCRouter({
|
||||||
|
example: exampleRouter,
|
||||||
|
auth: authRouter,
|
||||||
|
database: databaseRouter,
|
||||||
|
lineage: lineageRouter,
|
||||||
|
misc: miscRouter,
|
||||||
|
myRouter: myRouter, // Add this line
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use in frontend
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In your frontend component
|
||||||
|
const data = await api.myRouter.hello.query("World");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Type Safety**: Always use Zod schemas to validate input data and return types.
|
||||||
|
|
||||||
|
2. **Error Handling**: Implement proper error handling with try/catch blocks in async procedures.
|
||||||
|
|
||||||
|
3. **Procedure Organization**: Group related procedures into logical routers.
|
||||||
|
|
||||||
|
4. **Consistent Naming**: Use clear, descriptive names for your procedures and routers.
|
||||||
|
|
||||||
|
5. **Documentation**: Document each procedure with clear descriptions of what it does.
|
||||||
|
|
||||||
|
## Example Usage Patterns
|
||||||
|
|
||||||
|
### Query Procedure (GET)
|
||||||
|
```typescript
|
||||||
|
// In your router file
|
||||||
|
getPosts: publicProcedure
|
||||||
|
.input(z.object({
|
||||||
|
limit: z.number().optional(),
|
||||||
|
offset: z.number().optional()
|
||||||
|
}))
|
||||||
|
.query(({ input }) => {
|
||||||
|
// Return data from database or external service
|
||||||
|
return { posts: [], total: 0 };
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In frontend component
|
||||||
|
const { data, isLoading } = api.database.getPosts.useQuery({ limit: 10 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mutation Procedure (POST/PUT/DELETE)
|
||||||
|
```typescript
|
||||||
|
// In your router file
|
||||||
|
createPost: publicProcedure
|
||||||
|
.input(z.object({
|
||||||
|
title: z.string(),
|
||||||
|
content: z.string()
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
// Create post in database
|
||||||
|
return { success: true, post: { id: "1", ...input } };
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In frontend component
|
||||||
|
const { mutateAsync } = api.database.createPost.useMutation();
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
try {
|
||||||
|
const result = await mutateAsync({
|
||||||
|
title: "New Post",
|
||||||
|
content: "Post content"
|
||||||
|
});
|
||||||
|
console.log("Created post:", result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating post:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Endpoints
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
- `auth.githubCallback` - GitHub OAuth callback
|
||||||
|
- `auth.googleCallback` - Google OAuth callback
|
||||||
|
- `auth.emailLogin` - Email login
|
||||||
|
- `auth.emailVerification` - Email verification
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- `database.getCommentReactions` - Get comment reactions
|
||||||
|
- `database.postCommentReaction` - Add comment reaction
|
||||||
|
- `database.deleteCommentReaction` - Remove comment reaction
|
||||||
|
- `database.getComments` - Get comments for a post
|
||||||
|
- `database.getPosts` - Get posts with pagination
|
||||||
|
- `database.createPost` - Create new post
|
||||||
|
- `database.updatePost` - Update existing post
|
||||||
|
- `database.deletePost` - Delete post
|
||||||
|
- `database.getPostLikes` - Get likes for a post
|
||||||
|
- `database.likePost` - Like a post
|
||||||
|
- `database.unlikePost` - Unlike a post
|
||||||
|
|
||||||
|
### Lineage
|
||||||
|
- `lineage.databaseManagement` - Database management operations
|
||||||
|
- `lineage.analytics` - Analytics endpoints
|
||||||
|
- `lineage.appleAuth` - Apple authentication
|
||||||
|
- `lineage.emailLogin` - Email login
|
||||||
|
- `lineage.emailRegister` - Email registration
|
||||||
|
- `lineage.emailVerify` - Email verification
|
||||||
|
- `lineage.googleRegister` - Google registration
|
||||||
|
- `lineage.attacks` - Attack data
|
||||||
|
- `lineage.conditions` - Condition data
|
||||||
|
- `lineage.dungeons` - Dungeon data
|
||||||
|
- `lineage.enemies` - Enemy data
|
||||||
|
- `lineage.items` - Item data
|
||||||
|
- `lineage.misc` - Miscellaneous data
|
||||||
|
- `lineage.offlineSecret` - Offline secret endpoint
|
||||||
|
- `lineage.pvpGet` - PvP GET operations
|
||||||
|
- `lineage.pvpPost` - PvP POST operations
|
||||||
|
- `lineage.tokens` - Token operations
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
- `misc.downloads` - Downloads endpoint
|
||||||
|
- `misc.s3Delete` - Delete S3 object
|
||||||
|
- `misc.s3Get` - Get S3 object
|
||||||
|
- `misc.hashPassword` - Hash password
|
||||||
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "example-with-trpc",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vinxi dev",
|
||||||
|
"build": "vinxi build",
|
||||||
|
"start": "vinxi start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@solidjs/meta": "^0.29.4",
|
||||||
|
"@solidjs/router": "^0.15.0",
|
||||||
|
"@solidjs/start": "^1.1.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.7",
|
||||||
|
"@trpc/client": "^10.45.2",
|
||||||
|
"@trpc/server": "^10.45.2",
|
||||||
|
"@typeschema/valibot": "^0.13.4",
|
||||||
|
"solid-js": "^1.9.5",
|
||||||
|
"valibot": "^0.29.0",
|
||||||
|
"vinxi": "^0.5.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 664 B |
84
src/api/auth/callback/github/route.ts
Normal file
84
src/api/auth/callback/github/route.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { v4 as uuidV4 } from "uuid";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { User } from "@/types/model-types";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const params = request.nextUrl.searchParams;
|
||||||
|
const code = params.get("code");
|
||||||
|
if (code) {
|
||||||
|
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.NEXT_PUBLIC_GITHUB_CLIENT_ID,
|
||||||
|
client_secret: env.GITHUB_CLIENT_SECRET,
|
||||||
|
code,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { access_token } = await tokenResponse.json();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`;
|
||||||
|
const params = ["github", login];
|
||||||
|
const res = await conn.execute({ sql: query, args: params });
|
||||||
|
if (res.rows[0]) {
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: (res.rows[0] as unknown as User).id },
|
||||||
|
env.JWT_SECRET_KEY,
|
||||||
|
{
|
||||||
|
expiresIn: 60 * 60 * 24 * 14, // expires in 14 days
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(await cookies()).set({
|
||||||
|
name: "userIDToken",
|
||||||
|
value: token,
|
||||||
|
maxAge: 60 * 60 * 24 * 14,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const icon = user.avatar_url;
|
||||||
|
const email = user.email;
|
||||||
|
const 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 });
|
||||||
|
const token = jwt.sign({ id: userId }, env.JWT_SECRET_KEY, {
|
||||||
|
expiresIn: 60 * 60 * 24 * 14, // expires in 14 days
|
||||||
|
});
|
||||||
|
(await cookies()).set({
|
||||||
|
name: "userIDToken",
|
||||||
|
value: token,
|
||||||
|
maxAge: 60 * 60 * 24 * 14,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(`${env.NEXT_PUBLIC_DOMAIN}/account`);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: `authentication failed: no code on callback`,
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/api/auth/callback/google/route.ts
Normal file
99
src/api/auth/callback/google/route.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { v4 as uuidV4 } from "uuid";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { User } from "@/types/model-types";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const params = request.nextUrl.searchParams;
|
||||||
|
const code = params.get("code");
|
||||||
|
if (code) {
|
||||||
|
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.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
|
||||||
|
client_secret: env.GOOGLE_CLIENT_SECRET,
|
||||||
|
redirect_uri: "https://www.freno.me/api/auth/callback/google",
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { access_token } = await tokenResponse.json();
|
||||||
|
|
||||||
|
const userResponse = await fetch(
|
||||||
|
"https://www.googleapis.com/oauth2/v3/userinfo",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${access_token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const userData = await userResponse.json();
|
||||||
|
console.log(userData);
|
||||||
|
const name = userData.name;
|
||||||
|
const image = userData.picture;
|
||||||
|
const email = userData.email;
|
||||||
|
const email_verified = userData.email_verified;
|
||||||
|
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
|
const query = `SELECT * FROM User WHERE provider = ? AND email = ?`;
|
||||||
|
const params = ["google", email];
|
||||||
|
const res = await conn.execute({ sql: query, args: params });
|
||||||
|
if (res.rows[0]) {
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: (res.rows[0] as unknown as User).id },
|
||||||
|
env.JWT_SECRET_KEY,
|
||||||
|
{
|
||||||
|
expiresIn: 60 * 60 * 24 * 14, // expires in 14 days
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(await cookies()).set({
|
||||||
|
name: "userIDToken",
|
||||||
|
value: token,
|
||||||
|
maxAge: 60 * 60 * 24 * 14,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const 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,
|
||||||
|
});
|
||||||
|
const token = jwt.sign({ id: userId }, env.JWT_SECRET_KEY, {
|
||||||
|
expiresIn: 60 * 60 * 24 * 14, // expires in 14 days
|
||||||
|
});
|
||||||
|
(await cookies()).set({
|
||||||
|
name: "userIDToken",
|
||||||
|
value: token,
|
||||||
|
maxAge: 60 * 60 * 24 * 14,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(`${env.NEXT_PUBLIC_DOMAIN}/account`);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: `authentication failed: no code on callback`,
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/api/auth/email-login/[email]/route.ts
Normal file
64
src/api/auth/email-login/[email]/route.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { JwtPayload } from "jsonwebtoken";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { User } from "@/types/model-types";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: Promise<{ email: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const secretKey = env.JWT_SECRET_KEY;
|
||||||
|
const params = request.nextUrl.searchParams;
|
||||||
|
const token = params.get("token");
|
||||||
|
const userEmail = readyParams.email;
|
||||||
|
try {
|
||||||
|
if (token) {
|
||||||
|
const decoded = jwt.verify(token, secretKey) as JwtPayload;
|
||||||
|
if (decoded.email == userEmail) {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `SELECT * FROM User WHERE email = ?`;
|
||||||
|
const params = [decoded.email];
|
||||||
|
const res = await conn.execute({ sql: query, args: params });
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: (res.rows[0] as unknown as User).id },
|
||||||
|
env.JWT_SECRET_KEY,
|
||||||
|
{
|
||||||
|
expiresIn: 60 * 60 * 24 * 14, // expires in 14 days
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (decoded.rememberMe) {
|
||||||
|
(await cookies()).set({
|
||||||
|
name: "userIDToken",
|
||||||
|
value: token,
|
||||||
|
maxAge: 60 * 60 * 24 * 14,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
(await cookies()).set({
|
||||||
|
name: "userIDToken",
|
||||||
|
value: token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return NextResponse.redirect(`${env.NEXT_PUBLIC_DOMAIN}/account`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: `authentication failed: no token`,
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return NextResponse.json(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: `authentication failed: ${err}`,
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/api/auth/email-verification/[email]/route.ts
Normal file
49
src/api/auth/email-verification/[email]/route.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: Promise<{ email: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const secretKey = env.JWT_SECRET_KEY;
|
||||||
|
const params = request.nextUrl.searchParams;
|
||||||
|
const token = params.get("token");
|
||||||
|
const userEmail = readyParams.email;
|
||||||
|
try {
|
||||||
|
if (token) {
|
||||||
|
const decoded = jwt.verify(token, secretKey) as JwtPayload;
|
||||||
|
if (decoded.email == userEmail) {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `UPDATE User SET email_verified = ? WHERE email = ?`;
|
||||||
|
const params = [true, userEmail];
|
||||||
|
await conn.execute({ sql: query, args: params });
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "email verification success, you may close this window",
|
||||||
|
}),
|
||||||
|
{ status: 202, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: `authentication failed: no token`,
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Invalid token:", err);
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "authentication failed: Invalid token",
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/api/database/comment-reactions/[commentID]/route.ts
Normal file
21
src/api/database/comment-reactions/[commentID]/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_: Request,
|
||||||
|
context: { params: Promise<{ commentID: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const commentID = readyParams.commentID;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const commentQuery = "SELECT * FROM CommentReaction WHERE comment_id = ?";
|
||||||
|
const commentParams = [commentID];
|
||||||
|
const commentResults = await conn.execute({
|
||||||
|
sql: commentQuery,
|
||||||
|
args: commentParams,
|
||||||
|
});
|
||||||
|
return NextResponse.json(
|
||||||
|
{ commentReactions: commentResults.rows },
|
||||||
|
{ status: 202 },
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/api/database/comment-reactions/add/[type]/route.ts
Normal file
27
src/api/database/comment-reactions/add/[type]/route.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { CommentReactionInput } from "@/types/input-types";
|
||||||
|
import { CommentReaction } from "@/types/model-types";
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
input: NextRequest,
|
||||||
|
context: { params: Promise<{ type: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const inputData = (await input.json()) as CommentReactionInput;
|
||||||
|
const { comment_id, user_id } = inputData;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `
|
||||||
|
INSERT INTO CommentReaction (type, comment_id, user_id)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`;
|
||||||
|
const params = [readyParams.type, comment_id, user_id];
|
||||||
|
await conn.execute({ sql: query, args: params });
|
||||||
|
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
|
||||||
|
const followUpParams = [comment_id];
|
||||||
|
const res = await conn.execute({ sql: followUpQuery, args: followUpParams });
|
||||||
|
const data = (res.rows as unknown as CommentReaction[]).filter(
|
||||||
|
(commentReaction) => commentReaction.comment_id == comment_id,
|
||||||
|
);
|
||||||
|
return NextResponse.json({ commentReactions: data || [] });
|
||||||
|
}
|
||||||
28
src/api/database/comment-reactions/remove/[type]/route.ts
Normal file
28
src/api/database/comment-reactions/remove/[type]/route.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { CommentReactionInput } from "@/types/input-types";
|
||||||
|
import { CommentReaction } from "@/types/model-types";
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
input: NextRequest,
|
||||||
|
context: { params: Promise<{ type: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const inputData = (await input.json()) as CommentReactionInput;
|
||||||
|
const { comment_id, user_id } = inputData;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `
|
||||||
|
DELETE FROM CommentReaction
|
||||||
|
WHERE type = ? AND comment_id = ? AND user_id = ?
|
||||||
|
`;
|
||||||
|
const params = [readyParams.type, comment_id, user_id];
|
||||||
|
await conn.execute({ sql: query, args: params });
|
||||||
|
|
||||||
|
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
|
||||||
|
const followUpParams = [comment_id];
|
||||||
|
const res = await conn.execute({ sql: followUpQuery, args: followUpParams });
|
||||||
|
const data = (res.rows as unknown as CommentReaction[]).filter(
|
||||||
|
(commentReaction) => commentReaction.comment_id == comment_id,
|
||||||
|
);
|
||||||
|
return NextResponse.json({ commentReactions: data || [] });
|
||||||
|
}
|
||||||
16
src/api/database/comments/get-all/[post_id]/route.ts
Normal file
16
src/api/database/comments/get-all/[post_id]/route.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_: Request,
|
||||||
|
context: {
|
||||||
|
params: Promise<{ post_id: string }>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `SELECT * FROM Comment WHERE post_id = ?`;
|
||||||
|
const params = [readyParams.post_id];
|
||||||
|
const res = await conn.execute({ sql: query, args: params });
|
||||||
|
return NextResponse.json({ comments: res.rows }, { status: 302 });
|
||||||
|
}
|
||||||
9
src/api/database/comments/get-all/route.ts
Normal file
9
src/api/database/comments/get-all/route.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `SELECT * FROM Comment`;
|
||||||
|
const res = await conn.execute(query);
|
||||||
|
return NextResponse.json({ comments: res.rows });
|
||||||
|
}
|
||||||
17
src/api/database/post-like/add/route.ts
Normal file
17
src/api/database/post-like/add/route.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { PostLikeInput } from "@/types/input-types";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as PostLikeInput;
|
||||||
|
const { user_id, post_id } = inputData;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `INSERT INTO PostLike (user_id, post_id)
|
||||||
|
VALUES (?, ?)`;
|
||||||
|
const params = [user_id, post_id];
|
||||||
|
await conn.execute({ sql: query, args: params });
|
||||||
|
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
|
||||||
|
const followUpParams = [post_id];
|
||||||
|
const res = await conn.execute({ sql: followUpQuery, args: followUpParams });
|
||||||
|
return NextResponse.json({ newLikes: res.rows });
|
||||||
|
}
|
||||||
19
src/api/database/post-like/remove/route.ts
Normal file
19
src/api/database/post-like/remove/route.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { PostLikeInput } from "@/types/input-types";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as PostLikeInput;
|
||||||
|
const { user_id, post_id } = inputData;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `
|
||||||
|
DELETE FROM PostLike
|
||||||
|
WHERE user_id = ? AND post_id = ?
|
||||||
|
`;
|
||||||
|
const params = [user_id, post_id];
|
||||||
|
await conn.execute({ sql: query, args: params });
|
||||||
|
const followUpQuery = `SELECT * FROM PostLike WHERE post_id=?`;
|
||||||
|
const followUpParams = [post_id];
|
||||||
|
const res = await conn.execute({ sql: followUpQuery, args: followUpParams });
|
||||||
|
return NextResponse.json({ newLikes: res.rows });
|
||||||
|
}
|
||||||
43
src/api/database/post/[category]/by-id/[id]/route.ts
Normal file
43
src/api/database/post/[category]/by-id/[id]/route.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_: NextRequest,
|
||||||
|
context: { params: Promise<{ category: string; id: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
if (readyParams.category !== "blog" && readyParams.category !== "project") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "invalid category value" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `SELECT * FROM Post WHERE id = ?`;
|
||||||
|
const params = [parseInt(readyParams.id)];
|
||||||
|
const results = await conn.execute({ sql: query, args: params });
|
||||||
|
const tagQuery = `SELECT * FROM Tag WHERE post_id = ?`;
|
||||||
|
const tagRes = await conn.execute({ sql: tagQuery, args: params });
|
||||||
|
if (results.rows[0]) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
post: results.rows[0],
|
||||||
|
tags: tagRes.rows,
|
||||||
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
post: [],
|
||||||
|
},
|
||||||
|
{ status: 204 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ error: e }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/api/database/post/[category]/by-title/[title]/route.ts
Normal file
73
src/api/database/post/[category]/by-title/[title]/route.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { Post } from "@/types/model-types";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: Promise<{ category: string; title: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
if (readyParams.category !== "blog" && readyParams.category !== "project") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "invalid category value" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
let privilegeLevel = "anonymous";
|
||||||
|
const token = request.cookies.get("userIDToken");
|
||||||
|
if (token) {
|
||||||
|
if (token.value == env.ADMIN_ID) {
|
||||||
|
privilegeLevel = "admin";
|
||||||
|
} else {
|
||||||
|
privilegeLevel = "user";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
|
const projectQuery =
|
||||||
|
"SELECT p.*, c.*, l.*,t.* FROM Post p JOIN Comment c ON p.id = c.post_id JOIN PostLike l ON p.id = l.post_idJOIN Tag t ON p.id = t.post_id WHERE p.title = ? AND p.category = ? AND p.published = ?;";
|
||||||
|
const projectParams = [readyParams.title, readyParams.category, true];
|
||||||
|
const projectResults = await conn.execute({
|
||||||
|
sql: projectQuery,
|
||||||
|
args: projectParams,
|
||||||
|
});
|
||||||
|
if (projectResults.rows[0]) {
|
||||||
|
const post_id = (projectResults.rows[0] as unknown as Post).id;
|
||||||
|
|
||||||
|
const commentQuery = "SELECT * FROM Comment WHERE post_id = ?";
|
||||||
|
const commentResults = await conn.execute({
|
||||||
|
sql: commentQuery,
|
||||||
|
args: [post_id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const likeQuery = "SELECT * FROM PostLike WHERE post_id = ?";
|
||||||
|
const likeQueryResults = await conn.execute({
|
||||||
|
sql: likeQuery,
|
||||||
|
args: [post_id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagsQuery = "SELECT * FROM Tag WHERE post_id = ?";
|
||||||
|
const tagResults = await conn.execute({
|
||||||
|
sql: tagsQuery,
|
||||||
|
args: [post_id],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
project: projectResults.rows[0],
|
||||||
|
comments: commentResults.rows,
|
||||||
|
likes: likeQueryResults.rows,
|
||||||
|
tagResults: tagResults.rows,
|
||||||
|
privilegeLevel: privilegeLevel,
|
||||||
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json({ status: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json({ error: e }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
src/api/database/post/[category]/manipulation/route.ts
Normal file
157
src/api/database/post/[category]/manipulation/route.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
interface POSTInputData {
|
||||||
|
title: string;
|
||||||
|
subtitle: string | null;
|
||||||
|
body: string | null;
|
||||||
|
banner_photo: string | null;
|
||||||
|
published: boolean;
|
||||||
|
tags: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PATCHInputData {
|
||||||
|
id: number;
|
||||||
|
title: string | null;
|
||||||
|
subtitle: string | null;
|
||||||
|
body: string | null;
|
||||||
|
banner_photo: string | null;
|
||||||
|
published: boolean | null;
|
||||||
|
tags: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
input: NextRequest,
|
||||||
|
context: { params: Promise<{ category: string }> },
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
if (readyParams.category !== "blog" && readyParams.category !== "project") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "invalid category value" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const inputData = (await input.json()) as POSTInputData;
|
||||||
|
const { title, subtitle, body, banner_photo, published, tags } =
|
||||||
|
inputData;
|
||||||
|
const userIDCookie = (await cookies()).get("userIDToken");
|
||||||
|
const fullURL = env.NEXT_PUBLIC_AWS_BUCKET_STRING + banner_photo;
|
||||||
|
|
||||||
|
if (userIDCookie) {
|
||||||
|
const author_id = userIDCookie.value;
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `
|
||||||
|
INSERT INTO Post (title, category, subtitle, body, banner_photo, published, author_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
const params = [
|
||||||
|
title,
|
||||||
|
readyParams.category,
|
||||||
|
subtitle,
|
||||||
|
body,
|
||||||
|
banner_photo ? fullURL : null,
|
||||||
|
published,
|
||||||
|
author_id,
|
||||||
|
];
|
||||||
|
const results = await conn.execute({ sql: query, args: params });
|
||||||
|
if (tags) {
|
||||||
|
let query = "INSERT INTO Tag (value, post_id) VALUES ";
|
||||||
|
let values = tags.map(
|
||||||
|
(tag) => `("${tag}", ${results.lastInsertRowid})`,
|
||||||
|
);
|
||||||
|
query += values.join(", ");
|
||||||
|
await conn.execute(query);
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ data: results.lastInsertRowid },
|
||||||
|
{ status: 201 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json({ error: "no cookie" }, { status: 401 });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ error: e }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function PATCH(input: NextRequest) {
|
||||||
|
try {
|
||||||
|
const inputData = (await input.json()) as PATCHInputData;
|
||||||
|
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const { query, params } = await createUpdateQuery(inputData);
|
||||||
|
const results = await conn.execute({
|
||||||
|
sql: query,
|
||||||
|
args: params as string[],
|
||||||
|
});
|
||||||
|
const { tags, id } = inputData;
|
||||||
|
const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`;
|
||||||
|
await conn.execute({ sql: deleteTagsQuery, args: [id.toString()] });
|
||||||
|
if (tags) {
|
||||||
|
let query = "INSERT INTO Tag (value, post_id) VALUES ";
|
||||||
|
let values = tags.map((tag) => `("${tag}", ${id})`);
|
||||||
|
query += values.join(", ");
|
||||||
|
await conn.execute(query);
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ data: results.lastInsertRowid },
|
||||||
|
{ status: 201 },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ error: e }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUpdateQuery(data: PATCHInputData) {
|
||||||
|
const { id, title, subtitle, body, banner_photo, published } = data;
|
||||||
|
|
||||||
|
let query = "UPDATE Post SET ";
|
||||||
|
let params = [];
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
if (title !== null) {
|
||||||
|
query += first ? "title = ?" : ", title = ?";
|
||||||
|
params.push(title);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtitle !== null) {
|
||||||
|
query += first ? "subtitle = ?" : ", subtitle = ?";
|
||||||
|
params.push(subtitle);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body !== null) {
|
||||||
|
query += first ? "body = ?" : ", body = ?";
|
||||||
|
params.push(body);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (banner_photo !== null) {
|
||||||
|
query += first ? "banner_photo = ?" : ", banner_photo = ?";
|
||||||
|
if (banner_photo == "_DELETE_IMAGE_") {
|
||||||
|
params.push(undefined);
|
||||||
|
} else {
|
||||||
|
params.push(env.NEXT_PUBLIC_AWS_BUCKET_STRING + banner_photo);
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (published !== null) {
|
||||||
|
query += first ? "published = ?" : ", published = ?";
|
||||||
|
params.push(published);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
query += first ? "author_id = ?" : ", author_id = ?";
|
||||||
|
params.push((await cookies()).get("userIDToken")?.value);
|
||||||
|
|
||||||
|
query += " WHERE id = ?";
|
||||||
|
params.push(id);
|
||||||
|
|
||||||
|
return { query, params };
|
||||||
|
}
|
||||||
20
src/api/database/user/email/route.ts
Normal file
20
src/api/database/user/email/route.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { newEmailInput } from "@/types/input-types";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as newEmailInput;
|
||||||
|
const { id, newEmail } = inputData;
|
||||||
|
const oldEmail = (await cookies()).get("emailToken");
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`;
|
||||||
|
const params = [newEmail, id, oldEmail];
|
||||||
|
try {
|
||||||
|
const res = await conn.execute({ sql: query, args: params as string[] });
|
||||||
|
return NextResponse.json({ res: res }, { status: 202 });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return NextResponse.json({ status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/api/database/user/from-id/[id]/route.ts
Normal file
35
src/api/database/user/from-id/[id]/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { User } from "@/types/model-types";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
export async function GET(
|
||||||
|
_: Request,
|
||||||
|
context: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const userQuery = "SELECT * FROM User WHERE id =?";
|
||||||
|
const params = await context.params;
|
||||||
|
const userParams = [params.id];
|
||||||
|
const res = await conn.execute({ sql: userQuery, args: userParams });
|
||||||
|
if (res.rows[0]) {
|
||||||
|
const user = res.rows[0] as unknown as User;
|
||||||
|
if (user && user.display_name !== "user deleted")
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
emailVerified: user.email_verified,
|
||||||
|
image: user.image,
|
||||||
|
displayName: user.display_name,
|
||||||
|
provider: user.provider,
|
||||||
|
hasPassword: !!user.password_hash,
|
||||||
|
},
|
||||||
|
{ status: 202 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json({}, { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return NextResponse.json({}, { status: 200 });
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/api/database/user/image/[id]/route.ts
Normal file
33
src/api/database/user/image/[id]/route.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { changeImageInput } from "@/types/input-types";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_: Request,
|
||||||
|
context: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = "SELECT * FROM User WHERE id = ?";
|
||||||
|
const params = await context.params;
|
||||||
|
const idArr = [params.id];
|
||||||
|
const results = await conn.execute({ sql: query, args: idArr });
|
||||||
|
return NextResponse.json({ user: results.rows[0] }, { status: 200 });
|
||||||
|
}
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
const inputData = (await request.json()) as changeImageInput;
|
||||||
|
const { imageURL } = inputData;
|
||||||
|
try {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const query = `UPDATE User SET image = ? WHERE id = ?`;
|
||||||
|
const fullURL = env.NEXT_PUBLIC_AWS_BUCKET_STRING + imageURL;
|
||||||
|
const params = [imageURL ? fullURL : null, (await context.params).id];
|
||||||
|
await conn.execute({ sql: query, args: params });
|
||||||
|
return NextResponse.json({ res: "success" }, { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
return NextResponse.json({ res: err }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/api/database/user/public-data/[id]/route.ts
Normal file
31
src/api/database/user/public-data/[id]/route.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { User } from "@/types/model-types";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
export async function GET(
|
||||||
|
_: Request,
|
||||||
|
context: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const conn = ConnectionFactory();
|
||||||
|
const userQuery = "SELECT email, display_name, image FROM User WHERE id =?";
|
||||||
|
const params = await context.params;
|
||||||
|
const userParams = [params.id];
|
||||||
|
const res = await conn.execute({ sql: userQuery, args: userParams });
|
||||||
|
if (res.rows[0]) {
|
||||||
|
const user = res.rows[0] as unknown as User;
|
||||||
|
if (user && user.display_name !== "user deleted")
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
email: user.email,
|
||||||
|
image: user.image,
|
||||||
|
display_name: user.display_name,
|
||||||
|
},
|
||||||
|
{ status: 202 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json({}, { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return NextResponse.json({}, { status: 200 });
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/api/downloads/public/[asset_name]/route.ts
Normal file
43
src/api/downloads/public/[asset_name]/route.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
const assets: Record<string, string> = {
|
||||||
|
"shapes-with-abigail": "shapes-with-abigail.apk",
|
||||||
|
"magic-delve": "magic-delve.apk",
|
||||||
|
cork: "Cork.zip",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bucket = "frenomedownloads";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_: Request,
|
||||||
|
context: {
|
||||||
|
params: Promise<{ asset_name: string }>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const readyParams = await context.params;
|
||||||
|
const params = {
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: assets[readyParams.asset_name],
|
||||||
|
Expires: 60 * 60,
|
||||||
|
};
|
||||||
|
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 NextResponse.json({ downloadURL: signedUrl });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return NextResponse.json({ error: e }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/api/lineage/_database_mgmt/loose/route.ts
Normal file
40
src/api/lineage/_database_mgmt/loose/route.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { createClient as createAPIClient } from "@tursodatabase/api";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const IGNORE = ["frenome", "magic-delve-conductor"];
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
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();
|
||||||
|
console.log(all_dbs);
|
||||||
|
const dbs_to_delete = all_dbs.filter((db) => {
|
||||||
|
return !IGNORE.includes(db.name) && !linkedDatabaseUrls.includes(db.name);
|
||||||
|
});
|
||||||
|
//console.log("will delete:", dbs_to_delete);
|
||||||
|
} catch (e) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: e,
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/api/lineage/_database_mgmt/old/route.ts
Normal file
42
src/api/lineage/_database_mgmt/old/route.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
import { createClient as createAPIClient } from "@tursodatabase/api";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.rows.forEach(async (row) => {
|
||||||
|
const db_url = row.database_url;
|
||||||
|
|
||||||
|
await turso.databases.delete(db_url as string);
|
||||||
|
const query =
|
||||||
|
"UPDATE User SET database_url = ?, database_token = ?, db_destroy_date = ? WHERE id = ?";
|
||||||
|
const params = [null, null, null, row.id];
|
||||||
|
conn.execute({ sql: query, args: params });
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: e,
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/api/lineage/analytics/route.ts
Normal file
42
src/api/lineage/analytics/route.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const {
|
||||||
|
playerID,
|
||||||
|
dungeonProgression,
|
||||||
|
playerClass,
|
||||||
|
spellCount,
|
||||||
|
proficiencies,
|
||||||
|
jobs,
|
||||||
|
resistanceTable,
|
||||||
|
damageTable,
|
||||||
|
} = await req.json();
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
|
return NextResponse.json({ status: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
return NextResponse.json({ status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/api/lineage/apple/email/route.ts
Normal file
26
src/api/lineage/apple/email/route.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const { userString } = await req.json();
|
||||||
|
if (!userString) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Missing required fields",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 NextResponse.json(
|
||||||
|
{ success: true, email: res.rows[0].email },
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ success: false }, { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/api/lineage/apple/registration/route.ts
Normal file
134
src/api/lineage/apple/registration/route.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { LineageConnectionFactory, LineageDBInit } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { createClient as createAPIClient } from "@tursodatabase/api";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const { email, userString } = await request.json();
|
||||||
|
if (!userString) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Missing required fields",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbName;
|
||||||
|
let dbToken;
|
||||||
|
const conn = LineageConnectionFactory();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let checkUserQuery = "SELECT * FROM User WHERE apple_user_string = ?";
|
||||||
|
|
||||||
|
let args = [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 new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "User information updated",
|
||||||
|
email: checkUserResult.rows[0].email,
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "User update failed!",
|
||||||
|
}),
|
||||||
|
{ status: 418, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User doesn't exist, insert new user and init database
|
||||||
|
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 new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "New user created",
|
||||||
|
dbName,
|
||||||
|
dbToken,
|
||||||
|
}),
|
||||||
|
{ status: 201, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const turso = createAPIClient({
|
||||||
|
org: "mikefreno",
|
||||||
|
token: env.TURSO_DB_API_TOKEN,
|
||||||
|
});
|
||||||
|
await turso.databases.delete(dbName);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "An error occurred while processing the request",
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/api/lineage/database/creds/route.ts
Normal file
89
src/api/lineage/database/creds/route.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { OAuth2Client } from "google-auth-library";
|
||||||
|
const CLIENT_ID = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE;
|
||||||
|
|
||||||
|
const client = new OAuth2Client(CLIENT_ID);
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get("authorization");
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return new NextResponse(JSON.stringify({ valid: false }), { status: 401 });
|
||||||
|
}
|
||||||
|
const { email, provider } = await req.json();
|
||||||
|
if (!email) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ success: false, message: "missing email in body" }),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let valid_request = false;
|
||||||
|
if (provider == "email") {
|
||||||
|
const decoded = jwt.verify(token, env.JWT_SECRET_KEY) as jwt.JwtPayload;
|
||||||
|
if (decoded.email == email) {
|
||||||
|
valid_request = true;
|
||||||
|
}
|
||||||
|
} else if (provider == "google") {
|
||||||
|
const ticket = await client.verifyIdToken({
|
||||||
|
idToken: token,
|
||||||
|
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: [token] });
|
||||||
|
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 new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
db_name: user.database_name,
|
||||||
|
db_token: user.database_token,
|
||||||
|
}),
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ success: false, message: "no user found" }),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ success: false, message: "destroy token" }),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ success: false, message: error }),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/api/lineage/database/deletion/cancel/route.ts
Normal file
69
src/api/lineage/database/deletion/cancel/route.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { LineageConnectionFactory, validateLineageRequest } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get("authorization");
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 401,
|
||||||
|
ok: false,
|
||||||
|
message: "Missing or invalid authorization header.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const auth_token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
const { email } = await req.json();
|
||||||
|
if (!email) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 400,
|
||||||
|
ok: false,
|
||||||
|
message: "Email is required to cancel the cron job.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn = LineageConnectionFactory();
|
||||||
|
|
||||||
|
const resUser = await conn.execute({
|
||||||
|
sql: `SELECT * FROM User WHERE email = ?;`,
|
||||||
|
args: [email],
|
||||||
|
});
|
||||||
|
if (resUser.rows.length === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 404,
|
||||||
|
ok: false,
|
||||||
|
message: "User not found.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const userRow = resUser.rows[0];
|
||||||
|
if (!userRow) {
|
||||||
|
return NextResponse.json({ status: 404, ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await validateLineageRequest({ auth_token, userRow });
|
||||||
|
if (!valid) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 401,
|
||||||
|
ok: false,
|
||||||
|
message: "Invalid credentials for cancelation.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await conn.execute({
|
||||||
|
sql: `DELETE FROM cron WHERE email = ?;`,
|
||||||
|
args: [email],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.rowsAffected > 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
message: "Cron job(s) canceled successfully.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 404,
|
||||||
|
ok: false,
|
||||||
|
message: "No cron job found for the given email.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/api/lineage/database/deletion/check/route.ts
Normal file
24
src/api/lineage/database/deletion/check/route.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const { email } = await req.json();
|
||||||
|
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 NextResponse.json({ status: 204, ok: true });
|
||||||
|
}
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
created_at: cronRow.created_at,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json({ status: 500, ok: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/api/lineage/database/deletion/cron/route.ts
Normal file
73
src/api/lineage/database/deletion/cron/route.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { dumpAndSendDB, LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { createClient as createAPIClient } from "@tursodatabase/api";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
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 = [];
|
||||||
|
for (const row of res.rows) {
|
||||||
|
const { id, db_name, db_token, send_dump_target, email } = row;
|
||||||
|
|
||||||
|
if (send_dump_target) {
|
||||||
|
const res = await dumpAndSendDB({
|
||||||
|
dbName: db_name as string,
|
||||||
|
dbToken: db_token as string,
|
||||||
|
sendTarget: send_dump_target as string,
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
//const res = await turso.databases.delete(db_name as string);
|
||||||
|
//
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
executed_ids.push(id);
|
||||||
|
// Shouldn't fail. No idea what the response from turso would be at this point - not documented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
conn.execute({
|
||||||
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
|
args: [email],
|
||||||
|
});
|
||||||
|
executed_ids.push(id);
|
||||||
|
// Shouldn't fail. No idea what the response from turso would be at this point - not documented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
message:
|
||||||
|
"Processed databases deleted and corresponding cron rows removed.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json({ status: 200, ok: true });
|
||||||
|
}
|
||||||
154
src/api/lineage/database/deletion/init/route.ts
Normal file
154
src/api/lineage/database/deletion/init/route.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
dumpAndSendDB,
|
||||||
|
LineageConnectionFactory,
|
||||||
|
validateLineageRequest,
|
||||||
|
} from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get("authorization");
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return NextResponse.json({ status: 401, ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth_token = authHeader.split(" ")[1];
|
||||||
|
const { email, db_name, db_token, skip_cron, send_dump_target } =
|
||||||
|
await req.json();
|
||||||
|
if (!email || !db_name || !db_token || !auth_token) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 401,
|
||||||
|
message: "Missing required fields",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn = LineageConnectionFactory();
|
||||||
|
const res = await conn.execute({
|
||||||
|
sql: `SELECT * FROM User WHERE email = ?`,
|
||||||
|
args: [email],
|
||||||
|
});
|
||||||
|
const userRow = res.rows[0];
|
||||||
|
if (!userRow) {
|
||||||
|
return NextResponse.json({ status: 404, ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await validateLineageRequest({ auth_token, userRow });
|
||||||
|
if (!valid) {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: false,
|
||||||
|
status: 401,
|
||||||
|
message: "Invalid Verification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { database_token, database_name } = userRow;
|
||||||
|
|
||||||
|
if (database_token !== db_token || database_name !== db_name) {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: false,
|
||||||
|
status: 401,
|
||||||
|
message: "Incorrect Verification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip_cron) {
|
||||||
|
if (send_dump_target) {
|
||||||
|
const res = await dumpAndSendDB({
|
||||||
|
dbName: db_name,
|
||||||
|
dbToken: db_token,
|
||||||
|
sendTarget: send_dump_target,
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
//const turso = createAPIClient({
|
||||||
|
//org: "mikefreno",
|
||||||
|
//token: env.TURSO_DB_API_TOKEN,
|
||||||
|
//});
|
||||||
|
//const res = await turso.databases.delete(db_name); // seems unreliable, using rest api instead
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
conn.execute({
|
||||||
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
|
args: [email],
|
||||||
|
});
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Shouldn't fail. No idea what the response from turso would be at this point - not documented
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 500,
|
||||||
|
message: "Unknown",
|
||||||
|
ok: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
message: res.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//const turso = createAPIClient({
|
||||||
|
//org: "mikefreno",
|
||||||
|
//token: env.TURSO_DB_API_TOKEN,
|
||||||
|
//});
|
||||||
|
//const res = await turso.databases.delete(db_name);
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
conn.execute({
|
||||||
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
|
args: [email],
|
||||||
|
});
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
message: `Account and Database deleted`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Shouldn't fail. No idea what the response from turso would be at this point - not documented
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
message: "Unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
message: `Deletion scheduled.`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
message: `Deletion not scheduled, due to server failure`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/api/lineage/email/login/route.ts
Normal file
81
src/api/lineage/email/login/route.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { LINEAGE_JWT_EXPIRY, LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { checkPassword } from "../../../passwordHashing";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
interface InputData {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as InputData;
|
||||||
|
const { email, password } = inputData;
|
||||||
|
if (email && password) {
|
||||||
|
if (password.length < 8) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Invalid Credentials",
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Invalid Credentials",
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const user = res.rows[0];
|
||||||
|
if (user.email_verified === 0) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Email not yet verified!",
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const valid = await checkPassword(password, user.password_hash as string);
|
||||||
|
if (!valid) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Invalid Credentials",
|
||||||
|
}),
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create token
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user.id, email: user.email },
|
||||||
|
env.JWT_SECRET_KEY,
|
||||||
|
{ expiresIn: LINEAGE_JWT_EXPIRY },
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Login successful",
|
||||||
|
token: token,
|
||||||
|
email: email,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Missing required fields",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/api/lineage/email/refresh/token/route.ts
Normal file
33
src/api/lineage/email/refresh/token/route.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { LINEAGE_JWT_EXPIRY } from "@/app/utils";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get("authorization");
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return new NextResponse(JSON.stringify({ valid: false }), { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, env.JWT_SECRET_KEY) as jwt.JwtPayload;
|
||||||
|
|
||||||
|
const newToken = jwt.sign(
|
||||||
|
{ userId: decoded.userId, email: decoded.email },
|
||||||
|
env.JWT_SECRET_KEY,
|
||||||
|
{ expiresIn: LINEAGE_JWT_EXPIRY },
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
valid: true,
|
||||||
|
token: newToken,
|
||||||
|
email: decoded.email,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ status: 401, ok: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/api/lineage/email/refresh/verification/route.ts
Normal file
107
src/api/lineage/email/refresh/verification/route.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
interface InputData {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as InputData;
|
||||||
|
const { email } = inputData;
|
||||||
|
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) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Invalid Request",
|
||||||
|
}),
|
||||||
|
{ status: 409, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const email_res = await sendEmailVerification(email);
|
||||||
|
const json = await email_res.json();
|
||||||
|
if (json.messageId) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Email verification sent!",
|
||||||
|
}),
|
||||||
|
{ status: 201, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEmailVerification(userEmail: string) {
|
||||||
|
const apiKey = env.SENDINBLUE_KEY as string;
|
||||||
|
const apiUrl = "https://api.sendinblue.com/v3/smtp/email";
|
||||||
|
|
||||||
|
const secretKey = env.JWT_SECRET_KEY;
|
||||||
|
const payload = { email: userEmail };
|
||||||
|
const token = jwt.sign(payload, secretKey, { expiresIn: "15m" });
|
||||||
|
|
||||||
|
const sendinblueData = {
|
||||||
|
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=${env.NEXT_PUBLIC_DOMAIN}/api/lineage/email/verification/${userEmail}/?token=${token} class="button">Verify Email</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
subject: `Life and Lineage email verification`,
|
||||||
|
};
|
||||||
|
return await fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
"api-key": apiKey,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(sendinblueData),
|
||||||
|
});
|
||||||
|
}
|
||||||
144
src/api/lineage/email/registration/route.ts
Normal file
144
src/api/lineage/email/registration/route.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { hashPassword } from "../../../passwordHashing";
|
||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { LibsqlError } from "@libsql/client/web";
|
||||||
|
|
||||||
|
interface InputData {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
password_conf: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as InputData;
|
||||||
|
const { email, password, password_conf } = inputData;
|
||||||
|
|
||||||
|
if (email && password && password_conf) {
|
||||||
|
if (password == password_conf) {
|
||||||
|
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 res = await sendEmailVerification(email);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.messageId) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Email verification sent!",
|
||||||
|
}),
|
||||||
|
{ status: 201, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(json);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (e instanceof LibsqlError && e.code === "SQLITE_CONSTRAINT") {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "User already exists",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "An error occurred while creating the user",
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Password mismatch",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Missing required fields",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEmailVerification(userEmail: string) {
|
||||||
|
const apiKey = env.SENDINBLUE_KEY as string;
|
||||||
|
const apiUrl = "https://api.sendinblue.com/v3/smtp/email";
|
||||||
|
|
||||||
|
const secretKey = env.JWT_SECRET_KEY;
|
||||||
|
const payload = { email: userEmail };
|
||||||
|
const token = jwt.sign(payload, secretKey, { expiresIn: "15m" });
|
||||||
|
|
||||||
|
const sendinblueData = {
|
||||||
|
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=${env.NEXT_PUBLIC_DOMAIN}/api/lineage/email/verification/${userEmail}/?token=${token} class="button">Verify Email</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
subject: `Life and Lineage email verification`,
|
||||||
|
};
|
||||||
|
return await fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
"api-key": apiKey,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(sendinblueData),
|
||||||
|
});
|
||||||
|
}
|
||||||
96
src/api/lineage/email/verification/[email]/route.ts
Normal file
96
src/api/lineage/email/verification/[email]/route.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import { LineageConnectionFactory, LineageDBInit } from "@/app/utils";
|
||||||
|
import { createClient as createAPIClient } from "@tursodatabase/api";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: Promise<{ email: string }> },
|
||||||
|
) {
|
||||||
|
const secretKey = env.JWT_SECRET_KEY;
|
||||||
|
const params = request.nextUrl.searchParams;
|
||||||
|
const token = params.get("token");
|
||||||
|
const userEmail = (await context.params).email;
|
||||||
|
|
||||||
|
let conn;
|
||||||
|
let dbName;
|
||||||
|
let dbToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Authentication failed: no token" },
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, secretKey) as JwtPayload;
|
||||||
|
if (decoded.email !== userEmail) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Authentication failed: email mismatch" },
|
||||||
|
{ status: 401, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message:
|
||||||
|
"Email verification success. You may close this window and sign in within the app.",
|
||||||
|
}),
|
||||||
|
{ status: 202, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in email verification:", err);
|
||||||
|
|
||||||
|
// Delete the database if it was created
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to revert the User table update if conn is available
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Authentication failed: An error occurred during email verification. Please try again.",
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/api/lineage/google/registration/route.ts
Normal file
101
src/api/lineage/google/registration/route.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { LineageConnectionFactory, LineageDBInit } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { createClient as createAPIClient } from "@tursodatabase/api";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const { email } = await request.json();
|
||||||
|
if (!email) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Missing required fields",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn = LineageConnectionFactory();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the user exists
|
||||||
|
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 new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "User information updated",
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "User update failed!",
|
||||||
|
}),
|
||||||
|
{ status: 418, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User doesn't exist, insert new user and init database
|
||||||
|
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 new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "New user created",
|
||||||
|
}),
|
||||||
|
{ status: 201, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const turso = createAPIClient({
|
||||||
|
org: "mikefreno",
|
||||||
|
token: env.TURSO_DB_API_TOKEN,
|
||||||
|
});
|
||||||
|
await turso.databases.delete(db_name!);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in Google Sign-Up handler:", error);
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "An error occurred while processing the request",
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { "content-type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/api/lineage/json_service/attacks/route.ts
Normal file
27
src/api/lineage/json_service/attacks/route.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
playerAttacks,
|
||||||
|
mageBooks,
|
||||||
|
mageSpells,
|
||||||
|
necroBooks,
|
||||||
|
necroSpells,
|
||||||
|
rangerBooks,
|
||||||
|
rangerSpells,
|
||||||
|
paladinBooks,
|
||||||
|
paladinSpells,
|
||||||
|
summons,
|
||||||
|
});
|
||||||
|
}
|
||||||
13
src/api/lineage/json_service/conditions/route.ts
Normal file
13
src/api/lineage/json_service/conditions/route.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
conditions,
|
||||||
|
debilitations,
|
||||||
|
sanityDebuffs,
|
||||||
|
});
|
||||||
|
}
|
||||||
7
src/api/lineage/json_service/dungeons/route.ts
Normal file
7
src/api/lineage/json_service/dungeons/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import dungeons from "@/lineage-json/dungeon-route/dungeons.json";
|
||||||
|
import specialEncounters from "@/lineage-json/dungeon-route/specialEncounters.json";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ ok: true, dungeons, specialEncounters });
|
||||||
|
}
|
||||||
8
src/api/lineage/json_service/enemies/route.ts
Normal file
8
src/api/lineage/json_service/enemies/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ ok: true, bosses, enemies, enemyAttacks });
|
||||||
|
}
|
||||||
45
src/api/lineage/json_service/items/route.ts
Normal file
45
src/api/lineage/json_service/items/route.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
arrows,
|
||||||
|
bows,
|
||||||
|
foci,
|
||||||
|
hats,
|
||||||
|
junk,
|
||||||
|
melee,
|
||||||
|
robes,
|
||||||
|
wands,
|
||||||
|
ingredients,
|
||||||
|
storyItems,
|
||||||
|
artifacts,
|
||||||
|
shields,
|
||||||
|
bodyArmor,
|
||||||
|
helmets,
|
||||||
|
suffix,
|
||||||
|
prefix,
|
||||||
|
potions,
|
||||||
|
poison,
|
||||||
|
staves,
|
||||||
|
});
|
||||||
|
}
|
||||||
23
src/api/lineage/json_service/misc/route.ts
Normal file
23
src/api/lineage/json_service/misc/route.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
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 async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
activities,
|
||||||
|
investments,
|
||||||
|
jobs,
|
||||||
|
manaOptions,
|
||||||
|
otherOptions,
|
||||||
|
healthOptions,
|
||||||
|
sanityOptions,
|
||||||
|
pvpRewards,
|
||||||
|
});
|
||||||
|
}
|
||||||
5
src/api/lineage/offline_secret/route.ts
Normal file
5
src/api/lineage/offline_secret/route.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return new NextResponse(process.env.LINEAGE_OFFLINE_SERIALIZATION_SECRET);
|
||||||
|
}
|
||||||
28
src/api/lineage/pvp/battle_result/route.ts
Normal file
28
src/api/lineage/pvp/battle_result/route.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const { winnerLinkID, loserLinkID } = await req.json();
|
||||||
|
|
||||||
|
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 NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ ok: false, status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/api/lineage/pvp/route.ts
Normal file
154
src/api/lineage/pvp/route.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const { character, linkID, pushToken, pushCurrentlyEnabled } =
|
||||||
|
await req.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conn = LineageConnectionFactory();
|
||||||
|
const res = await conn.execute({
|
||||||
|
sql: `SELECT * FROM PvP_Characters WHERE linkID = ?`,
|
||||||
|
args: [linkID],
|
||||||
|
});
|
||||||
|
if (res.rows.length == 0) {
|
||||||
|
//create
|
||||||
|
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.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 NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
winCount: 0,
|
||||||
|
lossCount: 0,
|
||||||
|
tokenRedemptionCount: 0,
|
||||||
|
status: 201,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//update
|
||||||
|
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 NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
winCount: res.rows[0].winCount,
|
||||||
|
lossCount: res.rows[0].lossCount,
|
||||||
|
tokenRedemptionCount: res.rows[0].tokenRedemptionCount,
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ ok: false, status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Get three opponents, high, med, low, based on win/loss ratio
|
||||||
|
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 NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
characters: res.rows,
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ ok: false, status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/api/lineage/tokens/route.ts
Normal file
27
src/api/lineage/tokens/route.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { LineageConnectionFactory } from "@/app/utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const { token } = await req.json();
|
||||||
|
if (!token) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ success: false, message: "missing token in body" }),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 NextResponse.json(JSON.stringify(resUpdate));
|
||||||
|
} else {
|
||||||
|
const queryInsert = "INSERT INTO Token (token) VALUES (?)";
|
||||||
|
const resInsert = await conn.execute({ sql: queryInsert, args: [token] });
|
||||||
|
return NextResponse.json(JSON.stringify(resInsert));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/api/passwordHashing.ts
Normal file
20
src/api/passwordHashing.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as bcrypt from "bcrypt";
|
||||||
|
|
||||||
|
// Asynchronous function to hash a password
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
// 10 here is the number of rounds of hashing to apply
|
||||||
|
// The higher the number, the more secure but also the slower
|
||||||
|
const saltRounds = 10;
|
||||||
|
const salt = await bcrypt.genSalt(saltRounds);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
return hashedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asynchronous function to check a password against a hash
|
||||||
|
export async function checkPassword(
|
||||||
|
password: string,
|
||||||
|
hash: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const match = await bcrypt.compare(password, hash);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
35
src/api/s3/deleteImage/route.ts
Normal file
35
src/api/s3/deleteImage/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { NextRequest } from "next/dist/server/web/spec-extension/request";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { ConnectionFactory } from "@/app/utils";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
interface InputData {
|
||||||
|
key: string;
|
||||||
|
newAttachmentString: string;
|
||||||
|
type: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as InputData;
|
||||||
|
const { key, newAttachmentString, type, id } = inputData;
|
||||||
|
// Parse the url to get the bucket and key
|
||||||
|
|
||||||
|
const s3params = {
|
||||||
|
Bucket: env.AWS_S3_BUCKET_NAME,
|
||||||
|
Key: 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 ${type} SET attachments = ? WHERE id = ?`;
|
||||||
|
const dbparams = [newAttachmentString, id];
|
||||||
|
await conn.execute({ sql: query, args: dbparams });
|
||||||
|
return NextResponse.json(res);
|
||||||
|
}
|
||||||
41
src/api/s3/getPreSignedURL/route.ts
Normal file
41
src/api/s3/getPreSignedURL/route.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
interface InputData {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as InputData;
|
||||||
|
const { type, title, filename } = inputData;
|
||||||
|
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 = `${type}/${title}/${filename}`;
|
||||||
|
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||||
|
|
||||||
|
const s3params = {
|
||||||
|
Bucket: env.AWS_S3_BUCKET_NAME,
|
||||||
|
Key,
|
||||||
|
ContentType: `image/${ext![1]}`,
|
||||||
|
};
|
||||||
|
3;
|
||||||
|
const command = new PutObjectCommand(s3params);
|
||||||
|
|
||||||
|
const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 });
|
||||||
|
return NextResponse.json({ uploadURL: signedUrl, key: Key });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return NextResponse.json({ error: e }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/api/s3/simpleDeleteImage/route.ts
Normal file
31
src/api/s3/simpleDeleteImage/route.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { NextRequest } from "next/dist/server/web/spec-extension/request";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
interface InputData {
|
||||||
|
key: string;
|
||||||
|
newAttachmentString: string;
|
||||||
|
type: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(input: NextRequest) {
|
||||||
|
const inputData = (await input.json()) as InputData;
|
||||||
|
const { key } = inputData;
|
||||||
|
// Parse the url to get the bucket and key
|
||||||
|
|
||||||
|
const s3params = {
|
||||||
|
Bucket: env.AWS_S3_BUCKET_NAME,
|
||||||
|
Key: key,
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new S3Client({
|
||||||
|
region: env.AWS_REGION,
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new DeleteObjectCommand(s3params);
|
||||||
|
const res = await client.send(command);
|
||||||
|
return NextResponse.json(res);
|
||||||
|
}
|
||||||
168
src/app.css
Normal file
168
src/app.css
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Comments indicate what they are used for in vim/term
|
||||||
|
/* Base Colors (Background/Text) */
|
||||||
|
--color-base: #fbf1c7; /* Main background color (lightest) */
|
||||||
|
--color-mantle: #f3eac1; /* Secondary background (slightly darker) */
|
||||||
|
--color-crust: #e7deb7; /* Lowest background layer (darkest) */
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--color-text: #654735; /* Primary text color */
|
||||||
|
--color-subtext1: #7b5d44; /* Secondary text (comments, less important) */
|
||||||
|
--color-subtext0: #8f6f56; /* Tertiary text (even less important) */
|
||||||
|
|
||||||
|
/* Surface Colors */
|
||||||
|
--color-surface0: #dfd6b1; /* Surface layer 0 (medium background) */
|
||||||
|
--color-surface1: #c9c19f; /* Surface layer 1 (darker surface) */
|
||||||
|
--color-surface2: #a79c86; /* Surface layer 2 (darkest surface) */
|
||||||
|
|
||||||
|
/* Overlay Colors */
|
||||||
|
--color-overlay0: #c9aa8c; /* Overlay layer 0 */
|
||||||
|
--color-overlay1: #b6977a; /* Overlay layer 1 */
|
||||||
|
--color-overlay2: #a28368; /* Overlay layer 2 */
|
||||||
|
|
||||||
|
/* Accent Colors (Syntax/Highlighting) */
|
||||||
|
--color-red: #c14a4a; /* Error messages, important keywords */
|
||||||
|
--color-maroon: #c14a4a; /* Error messages, critical elements */
|
||||||
|
--color-peach: #c35e0a; /* Warning messages, operators */
|
||||||
|
--color-yellow: #a96b2c; /* Variables, parameters, attributes */
|
||||||
|
--color-green: #6c782e; /* Strings, literals, constants */
|
||||||
|
--color-teal: #4c7a5d; /* Functions, methods, built-ins */
|
||||||
|
--color-sky: #4c7a5d; /* Functions, methods (alternative) */
|
||||||
|
--color-sapphire: #4c7a5d; /* Functions, methods (alternative) */
|
||||||
|
--color-blue: #45707a; /* Keywords, types, classes */
|
||||||
|
--color-lavender: #45707a; /* Comments, documentation strings */
|
||||||
|
--color-pink: #945e80; /* Special syntax elements, identifiers */
|
||||||
|
--color-mauve: #945e80; /* Special syntax elements (alternative) */
|
||||||
|
--color-flamingo: #c14a4a; /* Error messages, critical elements */
|
||||||
|
--color-rosewater: #c14a4a; /* Error messages, critical elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-rosewater: #c14a4a;
|
||||||
|
--color-flamingo: #c14a4a;
|
||||||
|
--color-pink: #945e80;
|
||||||
|
--color-mauve: #945e80;
|
||||||
|
--color-red: #c14a4a;
|
||||||
|
--color-maroon: #c14a4a;
|
||||||
|
--color-peach: #c35e0a;
|
||||||
|
--color-yellow: #a96b2c;
|
||||||
|
--color-green: #6c782e;
|
||||||
|
--color-teal: #4c7a5d;
|
||||||
|
--color-sky: #4c7a5d;
|
||||||
|
--color-sapphire: #4c7a5d;
|
||||||
|
--color-blue: #45707a;
|
||||||
|
--color-lavender: #45707a;
|
||||||
|
--color-text: #654735;
|
||||||
|
--color-subtext1: #7b5d44;
|
||||||
|
--color-subtext0: #8f6f56;
|
||||||
|
--color-overlay2: #a28368;
|
||||||
|
--color-overlay1: #b6977a;
|
||||||
|
--color-overlay0: #c9aa8c;
|
||||||
|
--color-surface2: #a79c86;
|
||||||
|
--color-surface1: #c9c19f;
|
||||||
|
--color-surface0: #dfd6b1;
|
||||||
|
--color-base: #fbf1c7;
|
||||||
|
--color-mantle: #f3eac1;
|
||||||
|
--color-crust: #e7deb7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-rosewater: #efc9c2;
|
||||||
|
--color-flamingo: #ebb2b2;
|
||||||
|
--color-pink: #f2a7de;
|
||||||
|
--color-mauve: #b889f4;
|
||||||
|
--color-red: #ea7183;
|
||||||
|
--color-maroon: #ea838c;
|
||||||
|
--color-peach: #f39967;
|
||||||
|
--color-yellow: #eaca89;
|
||||||
|
--color-green: #96d382;
|
||||||
|
--color-teal: #78cec1;
|
||||||
|
--color-sky: #91d7e3;
|
||||||
|
--color-sapphire: #68bae0;
|
||||||
|
--color-blue: #739df2;
|
||||||
|
--color-lavender: #a0a8f6;
|
||||||
|
--color-text: #b5c1f1;
|
||||||
|
--color-subtext1: #a6b0d8;
|
||||||
|
--color-subtext0: #959ec2;
|
||||||
|
--color-overlay2: #848cad;
|
||||||
|
--color-overlay1: #717997;
|
||||||
|
--color-overlay0: #63677f;
|
||||||
|
--color-surface2: #505469;
|
||||||
|
--color-surface1: #3e4255;
|
||||||
|
--color-surface0: #2c2f40;
|
||||||
|
--color-base: #1e1e2e;
|
||||||
|
--color-mantle: #141620;
|
||||||
|
--color-crust: #0e0f16;
|
||||||
|
}
|
||||||
|
@theme {
|
||||||
|
--color-rosewater: #efc9c2;
|
||||||
|
--color-flamingo: #ebb2b2;
|
||||||
|
--color-pink: #f2a7de;
|
||||||
|
--color-mauve: #b889f4;
|
||||||
|
--color-red: #ea7183;
|
||||||
|
--color-maroon: #ea838c;
|
||||||
|
--color-peach: #f39967;
|
||||||
|
--color-yellow: #eaca89;
|
||||||
|
--color-green: #96d382;
|
||||||
|
--color-teal: #78cec1;
|
||||||
|
--color-sky: #91d7e3;
|
||||||
|
--color-sapphire: #68bae0;
|
||||||
|
--color-blue: #739df2;
|
||||||
|
--color-lavender: #a0a8f6;
|
||||||
|
--color-text: #b5c1f1;
|
||||||
|
--color-subtext1: #a6b0d8;
|
||||||
|
--color-subtext0: #959ec2;
|
||||||
|
--color-overlay2: #848cad;
|
||||||
|
--color-overlay1: #717997;
|
||||||
|
--color-overlay0: #63677f;
|
||||||
|
--color-surface2: #505469;
|
||||||
|
--color-surface1: #3e4255;
|
||||||
|
--color-surface0: #2c2f40;
|
||||||
|
--color-base: #1e1e2e;
|
||||||
|
--color-mantle: #141620;
|
||||||
|
--color-crust: #0e0f16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: "Source Code Pro", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--color-base);
|
||||||
|
color: var(--color-crust);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-typing {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
background-color: var(--color-text);
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
margin-left: 2px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block cursor when done typing */
|
||||||
|
.cursor-block {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1ch;
|
||||||
|
background-color: var(--color-text);
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
margin-left: 2px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%,
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
51%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app.tsx
Normal file
30
src/app.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Router } from "@solidjs/router";
|
||||||
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
|
import { Suspense } from "solid-js";
|
||||||
|
import "./app.css";
|
||||||
|
import { LeftBar, RightBar } from "./components/Bars";
|
||||||
|
import { TerminalSplash } from "./components/TerminalSplash";
|
||||||
|
import { SplashProvider } from "./context/splash";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<SplashProvider>
|
||||||
|
<div>
|
||||||
|
<TerminalSplash />
|
||||||
|
<Router
|
||||||
|
root={(props) => (
|
||||||
|
<div class="flex flex-row max-w-screen">
|
||||||
|
<LeftBar />
|
||||||
|
<div class="flex-1">
|
||||||
|
<Suspense>{props.children}</Suspense>
|
||||||
|
</div>
|
||||||
|
<RightBar />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileRoutes />
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
</SplashProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/Bars.tsx
Normal file
43
src/components/Bars.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Typewriter } from "./Typewriter";
|
||||||
|
|
||||||
|
export function LeftBar() {
|
||||||
|
return (
|
||||||
|
<nav class="w-fit max-w-[25%] min-h-screen h-full border-r-2 border-r-maroon flex flex-col text-text text-xl font-bold py-10 px-4 gap-4 text-left">
|
||||||
|
<Typewriter keepAlive={false}>
|
||||||
|
<h3 class="text-2xl">Left Navigation</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||||
|
<a href="#home">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||||
|
<a href="#about">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||||
|
<a href="#services">Services</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Typewriter>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RightBar() {
|
||||||
|
return (
|
||||||
|
<nav class="w-fit max-w-[25%] min-h-screen h-full border-l-2 border-l-maroon flex flex-col text-text text-xl font-bold py-10 px-4 gap-4 text-right">
|
||||||
|
<Typewriter keepAlive={false}>
|
||||||
|
<h3 class="text-2xl">Right Navigation</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||||
|
<a href="#home">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||||
|
<a href="#about">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:-translate-y-0.5 transition-transform duration-200 ease-in-out hover:text-green hover:font-bold hover:scale-110">
|
||||||
|
<a href="#services">Services</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Typewriter>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
src/components/TerminalSplash.tsx
Normal file
57
src/components/TerminalSplash.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Show, onMount, onCleanup, createSignal } from "solid-js";
|
||||||
|
import { useSplash } from "../context/splash";
|
||||||
|
|
||||||
|
const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
|
||||||
|
export function TerminalSplash() {
|
||||||
|
const { showSplash, setShowSplash } = useSplash();
|
||||||
|
const [showing, setShowing] = createSignal(0);
|
||||||
|
const [isVisible, setIsVisible] = createSignal(true);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setShowing((prev) => (prev + 1) % spinnerChars.length);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// Hide splash after 1.5 seconds
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setShowSplash(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle fade out when splash is hidden
|
||||||
|
const shouldRender = () => showSplash() || isVisible();
|
||||||
|
|
||||||
|
// Trigger fade out, then hide after transition
|
||||||
|
const opacity = () => {
|
||||||
|
if (!showSplash() && isVisible()) {
|
||||||
|
setTimeout(() => setIsVisible(false), 500);
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
if (showSplash()) {
|
||||||
|
setIsVisible(true);
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
return "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={shouldRender()}>
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50 w-screen h-screen bg-base flex overflow-hidden flex-col items-center justify-center mx-auto transition-opacity duration-500"
|
||||||
|
style={{ opacity: opacity() }}
|
||||||
|
>
|
||||||
|
<div class="font-mono text-text text-4xl whitespace-pre-wrap p-8 max-w-3xl">
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
{spinnerChars[showing()]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
src/components/Typewriter.tsx
Normal file
164
src/components/Typewriter.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { JSX, onMount, createSignal, children } from "solid-js";
|
||||||
|
import { useSplash } from "~/context/splash";
|
||||||
|
|
||||||
|
export function Typewriter(props: {
|
||||||
|
children: JSX.Element;
|
||||||
|
speed?: number;
|
||||||
|
class?: string;
|
||||||
|
keepAlive?: boolean | number;
|
||||||
|
delay?: number;
|
||||||
|
}) {
|
||||||
|
const { keepAlive = true, delay = 0 } = props;
|
||||||
|
let containerRef: HTMLDivElement | undefined;
|
||||||
|
let cursorRef: HTMLDivElement | undefined;
|
||||||
|
const [isTyping, setIsTyping] = createSignal(false);
|
||||||
|
const [isDelaying, setIsDelaying] = createSignal(delay > 0);
|
||||||
|
const [keepAliveCountdown, setKeepAliveCountdown] = createSignal(
|
||||||
|
typeof keepAlive === "number" ? keepAlive : -1,
|
||||||
|
);
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
const { showSplash } = useSplash();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!containerRef || !cursorRef) return;
|
||||||
|
|
||||||
|
// FIRST: Walk DOM and hide all text immediately
|
||||||
|
const textNodes: { node: Text; text: string; startIndex: number }[] = [];
|
||||||
|
let totalChars = 0;
|
||||||
|
|
||||||
|
const walkDOM = (node: Node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = node.textContent || "";
|
||||||
|
if (text.trim().length > 0) {
|
||||||
|
textNodes.push({
|
||||||
|
node: node as Text,
|
||||||
|
text: text,
|
||||||
|
startIndex: totalChars,
|
||||||
|
});
|
||||||
|
totalChars += text.length;
|
||||||
|
|
||||||
|
// Replace text with spans for each character
|
||||||
|
const span = document.createElement("span");
|
||||||
|
text.split("").forEach((char, i) => {
|
||||||
|
const charSpan = document.createElement("span");
|
||||||
|
charSpan.textContent = char;
|
||||||
|
charSpan.style.opacity = "0";
|
||||||
|
charSpan.setAttribute(
|
||||||
|
"data-char-index",
|
||||||
|
String(totalChars - text.length + i),
|
||||||
|
);
|
||||||
|
span.appendChild(charSpan);
|
||||||
|
});
|
||||||
|
node.parentNode?.replaceChild(span, node);
|
||||||
|
}
|
||||||
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
Array.from(node.childNodes).forEach(walkDOM);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walkDOM(containerRef);
|
||||||
|
|
||||||
|
// Position cursor at the first character location
|
||||||
|
const firstChar = containerRef.querySelector(
|
||||||
|
'[data-char-index="0"]',
|
||||||
|
) as HTMLElement;
|
||||||
|
if (firstChar && cursorRef) {
|
||||||
|
// Insert cursor before the first character
|
||||||
|
firstChar.parentNode?.insertBefore(cursorRef, firstChar);
|
||||||
|
// Set cursor height to match first character
|
||||||
|
cursorRef.style.height = `${firstChar.offsetHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THEN: Wait for splash to be hidden before starting the animation
|
||||||
|
const checkSplashHidden = () => {
|
||||||
|
if (showSplash()) {
|
||||||
|
setTimeout(checkSplashHidden, 10);
|
||||||
|
} else {
|
||||||
|
// Start delay if specified
|
||||||
|
if (delay > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsDelaying(false);
|
||||||
|
startReveal();
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
startReveal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startReveal = () => {
|
||||||
|
setIsTyping(true); // Switch to typing cursor
|
||||||
|
|
||||||
|
// Animate revealing characters
|
||||||
|
let currentIndex = 0;
|
||||||
|
const speed = props.speed || 30;
|
||||||
|
|
||||||
|
const revealNextChar = () => {
|
||||||
|
if (currentIndex < totalChars) {
|
||||||
|
const charSpan = containerRef?.querySelector(
|
||||||
|
`[data-char-index="${currentIndex}"]`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (charSpan) {
|
||||||
|
charSpan.style.opacity = "1";
|
||||||
|
|
||||||
|
// Move cursor after this character and match its height
|
||||||
|
if (cursorRef) {
|
||||||
|
charSpan.parentNode?.insertBefore(
|
||||||
|
cursorRef,
|
||||||
|
charSpan.nextSibling,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Match the height of the current character
|
||||||
|
const charHeight = charSpan.offsetHeight;
|
||||||
|
cursorRef.style.height = `${charHeight}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
|
setTimeout(revealNextChar, 1000 / speed);
|
||||||
|
} else {
|
||||||
|
// Typing finished, switch to block cursor
|
||||||
|
setIsTyping(false);
|
||||||
|
|
||||||
|
// Start keepAlive countdown if it's a number
|
||||||
|
if (typeof keepAlive === "number") {
|
||||||
|
const keepAliveInterval = setInterval(() => {
|
||||||
|
setKeepAliveCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(keepAliveInterval);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(revealNextChar, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkSplashHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCursorClass = () => {
|
||||||
|
if (isDelaying()) return "cursor-block"; // Blinking block during delay
|
||||||
|
if (isTyping()) return "cursor-typing"; // Thin line while typing
|
||||||
|
|
||||||
|
// After typing is done
|
||||||
|
if (typeof keepAlive === "number") {
|
||||||
|
return keepAliveCountdown() > 0 ? "cursor-block" : "hidden";
|
||||||
|
}
|
||||||
|
return keepAlive ? "cursor-block" : "hidden";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} class={props.class}>
|
||||||
|
{resolved()}
|
||||||
|
<span ref={cursorRef} class={getCursorClass()}>
|
||||||
|
{" "}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/context/splash.tsx
Normal file
29
src/context/splash.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Accessor, createContext, useContext } from "solid-js";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
// Create context with initial value
|
||||||
|
const SplashContext = createContext<{
|
||||||
|
showSplash: Accessor<boolean>;
|
||||||
|
setShowSplash: (show: boolean) => void;
|
||||||
|
}>({
|
||||||
|
showSplash: () => true,
|
||||||
|
setShowSplash: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useSplash() {
|
||||||
|
const context = useContext(SplashContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSplash must be used within a SplashProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SplashProvider(props: { children: any }) {
|
||||||
|
const [showSplash, setShowSplash] = createSignal(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplashContext.Provider value={{ showSplash, setShowSplash }}>
|
||||||
|
{props.children}
|
||||||
|
</SplashContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
src/entry-client.tsx
Normal file
4
src/entry-client.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { mount, StartClient } from "@solidjs/start/client";
|
||||||
|
|
||||||
|
mount(() => <StartClient />, document.getElementById("app")!);
|
||||||
29
src/entry-server.tsx
Normal file
29
src/entry-server.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||||
|
import { validateServerEnv } from "./env/server";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validatedEnv = validateServerEnv(import.meta.env);
|
||||||
|
console.log("Environment validation successful");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Environment validation failed:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createHandler(() => (
|
||||||
|
<StartServer
|
||||||
|
document={({ assets, children, scripts }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
{assets}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">{children}</div>
|
||||||
|
{scripts}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
226
src/env/server.ts
vendored
Normal file
226
src/env/server.ts
vendored
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const serverEnvSchema = z.object({
|
||||||
|
// Server-side environment variables
|
||||||
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
|
ADMIN_EMAIL: z.string().min(1),
|
||||||
|
ADMIN_ID: z.string().min(1),
|
||||||
|
JWT_SECRET_KEY: z.string().min(1),
|
||||||
|
DANGEROUS_DBCOMMAND_PASSWORD: z.string().min(1),
|
||||||
|
AWS_REGION: z.string().min(1),
|
||||||
|
AWS_S3_BUCKET_NAME: z.string().min(1),
|
||||||
|
_AWS_ACCESS_KEY: z.string().min(1),
|
||||||
|
_AWS_SECRET_KEY: z.string().min(1),
|
||||||
|
GOOGLE_CLIENT_SECRET: z.string().min(1),
|
||||||
|
GITHUB_CLIENT_SECRET: z.string().min(1),
|
||||||
|
EMAIL_SERVER: z.string().min(1),
|
||||||
|
EMAIL_FROM: z.string().min(1),
|
||||||
|
SENDINBLUE_KEY: z.string().min(1),
|
||||||
|
TURSO_DB_URL: z.string().min(1),
|
||||||
|
TURSO_DB_TOKEN: z.string().min(1),
|
||||||
|
TURSO_LINEAGE_URL: z.string().min(1),
|
||||||
|
TURSO_LINEAGE_TOKEN: z.string().min(1),
|
||||||
|
TURSO_DB_API_TOKEN: z.string().min(1),
|
||||||
|
LINEAGE_OFFLINE_SERIALIZATION_SECRET: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientEnvSchema = z.object({
|
||||||
|
// Client-side environment variables (using VITE_ prefix for SolidStart)
|
||||||
|
VITE_DOMAIN: z.string().min(1),
|
||||||
|
VITE_AWS_BUCKET_STRING: z.string().min(1),
|
||||||
|
VITE_GOOGLE_CLIENT_ID: z.string().min(1),
|
||||||
|
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1),
|
||||||
|
VITE_GITHUB_CLIENT_ID: z.string().min(1),
|
||||||
|
VITE_WEBSOCKET: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combined environment schema
|
||||||
|
export const envSchema = z.object({
|
||||||
|
server: serverEnvSchema,
|
||||||
|
client: clientEnvSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type inference
|
||||||
|
export type ServerEnv = z.infer<typeof serverEnvSchema>;
|
||||||
|
export type ClientEnv = z.infer<typeof clientEnvSchema>;
|
||||||
|
|
||||||
|
// Custom error class for better error handling
|
||||||
|
class EnvironmentError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public errors?: z.ZodFormattedError<any>,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "EnvironmentError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation function for server-side with detailed error messages
|
||||||
|
export const validateServerEnv = (
|
||||||
|
envVars: Record<string, string | undefined>,
|
||||||
|
): ServerEnv => {
|
||||||
|
try {
|
||||||
|
return serverEnvSchema.parse(envVars);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
const formattedErrors = error.format();
|
||||||
|
const missingVars = Object.entries(formattedErrors)
|
||||||
|
.filter(
|
||||||
|
([_, value]) =>
|
||||||
|
value._errors.length > 0 && value._errors[0] === "Required",
|
||||||
|
)
|
||||||
|
.map(([key, _]) => key);
|
||||||
|
|
||||||
|
const invalidVars = Object.entries(formattedErrors)
|
||||||
|
.filter(
|
||||||
|
([_, value]) =>
|
||||||
|
value._errors.length > 0 && value._errors[0] !== "Required",
|
||||||
|
)
|
||||||
|
.map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
error: value._errors[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
let errorMessage = "Environment validation failed:\n";
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
errorMessage += `Missing required variables: ${missingVars.join(
|
||||||
|
", ",
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidVars.length > 0) {
|
||||||
|
errorMessage += "Invalid values:\n";
|
||||||
|
invalidVars.forEach(({ key, error }) => {
|
||||||
|
errorMessage += ` ${key}: ${error}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EnvironmentError(errorMessage, formattedErrors);
|
||||||
|
}
|
||||||
|
throw new EnvironmentError(
|
||||||
|
"Environment validation failed with unknown error",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation function for client-side (runtime) with detailed error messages
|
||||||
|
export const validateClientEnv = (
|
||||||
|
envVars: Record<string, string | undefined>,
|
||||||
|
): ClientEnv => {
|
||||||
|
try {
|
||||||
|
return clientEnvSchema.parse(envVars);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
const formattedErrors = error.format();
|
||||||
|
const missingVars = Object.entries(formattedErrors)
|
||||||
|
.filter(
|
||||||
|
([_, value]) =>
|
||||||
|
value._errors.length > 0 && value._errors[0] === "Required",
|
||||||
|
)
|
||||||
|
.map(([key, _]) => key);
|
||||||
|
|
||||||
|
const invalidVars = Object.entries(formattedErrors)
|
||||||
|
.filter(
|
||||||
|
([_, value]) =>
|
||||||
|
value._errors.length > 0 && value._errors[0] !== "Required",
|
||||||
|
)
|
||||||
|
.map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
error: value._errors[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
let errorMessage = "Client environment validation failed:\n";
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
errorMessage += `Missing required variables: ${missingVars.join(
|
||||||
|
", ",
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidVars.length > 0) {
|
||||||
|
errorMessage += "Invalid values:\n";
|
||||||
|
invalidVars.forEach(({ key, error }) => {
|
||||||
|
errorMessage += ` ${key}: ${error}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EnvironmentError(errorMessage, formattedErrors);
|
||||||
|
}
|
||||||
|
throw new EnvironmentError(
|
||||||
|
"Client environment validation failed with unknown error",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Environment validation for server startup with better error reporting
|
||||||
|
export const env = (() => {
|
||||||
|
try {
|
||||||
|
// Validate server environment variables
|
||||||
|
const validatedServerEnv = validateServerEnv(import.meta.env);
|
||||||
|
|
||||||
|
console.log("✅ Environment validation successful");
|
||||||
|
return validatedServerEnv;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof EnvironmentError) {
|
||||||
|
console.error("❌ Environment validation failed:", error.message);
|
||||||
|
if (error.errors) {
|
||||||
|
console.error(
|
||||||
|
"Detailed errors:",
|
||||||
|
JSON.stringify(error.errors, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Environment validation failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
console.error("❌ Unexpected environment validation error:", error);
|
||||||
|
throw new Error("Unexpected environment validation error occurred");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// For client-side validation (useful in components)
|
||||||
|
export const getClientEnvValidation = () => {
|
||||||
|
try {
|
||||||
|
return validateClientEnv(import.meta.env);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof EnvironmentError) {
|
||||||
|
console.error("❌ Client environment validation failed:", error.message);
|
||||||
|
throw new Error(`Client environment validation failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
throw new Error("Client environment validation failed with unknown error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if a variable is missing
|
||||||
|
export const isMissingEnvVar = (varName: string): boolean => {
|
||||||
|
return !import.meta.env[varName] || import.meta.env[varName]?.trim() === "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get all missing environment variables
|
||||||
|
export const getMissingEnvVars = (): string[] => {
|
||||||
|
const requiredVars = [
|
||||||
|
"NODE_ENV",
|
||||||
|
"ADMIN_EMAIL",
|
||||||
|
"ADMIN_ID",
|
||||||
|
"JWT_SECRET_KEY",
|
||||||
|
"DANGEROUS_DBCOMMAND_PASSWORD",
|
||||||
|
"AWS_REGION",
|
||||||
|
"AWS_S3_BUCKET_NAME",
|
||||||
|
"_AWS_ACCESS_KEY",
|
||||||
|
"_AWS_SECRET_KEY",
|
||||||
|
"GOOGLE_CLIENT_SECRET",
|
||||||
|
"GITHUB_CLIENT_SECRET",
|
||||||
|
"EMAIL_SERVER",
|
||||||
|
"EMAIL_FROM",
|
||||||
|
"SENDINBLUE_KEY",
|
||||||
|
"TURSO_DB_URL",
|
||||||
|
"TURSO_DB_TOKEN",
|
||||||
|
"TURSO_LINEAGE_URL",
|
||||||
|
"TURSO_LINEAGE_TOKEN",
|
||||||
|
"TURSO_DB_API_TOKEN",
|
||||||
|
"LINEAGE_OFFLINE_SERIALIZATION_SECRET",
|
||||||
|
];
|
||||||
|
|
||||||
|
return requiredVars.filter((varName) => isMissingEnvVar(varName));
|
||||||
|
};
|
||||||
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@solidjs/start/env" />
|
||||||
23
src/lib/api.ts
Normal file
23
src/lib/api.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
createTRPCProxyClient,
|
||||||
|
httpBatchLink,
|
||||||
|
loggerLink,
|
||||||
|
} from '@trpc/client';
|
||||||
|
import { AppRouter } from "~/server/api/root";
|
||||||
|
|
||||||
|
const getBaseUrl = () => {
|
||||||
|
if (typeof window !== "undefined") return "";
|
||||||
|
// replace example.com with your actual production url
|
||||||
|
if (process.env.NODE_ENV === "production") return "https://example.com";
|
||||||
|
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// create the client, export it
|
||||||
|
export const api = createTRPCProxyClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
// will print out helpful logs when using client
|
||||||
|
loggerLink(),
|
||||||
|
// identifies what url will handle trpc requests
|
||||||
|
httpBatchLink({ url: `${getBaseUrl()}/api/trpc` })
|
||||||
|
],
|
||||||
|
});
|
||||||
19
src/routes/[...404].tsx
Normal file
19
src/routes/[...404].tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Title } from "@solidjs/meta";
|
||||||
|
import { HttpStatusCode } from "@solidjs/start";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Title>Not Found</Title>
|
||||||
|
<HttpStatusCode code={404} />
|
||||||
|
<h1>Page Not Found</h1>
|
||||||
|
<p>
|
||||||
|
Visit{" "}
|
||||||
|
<a href="https://start.solidjs.com" target="_blank">
|
||||||
|
start.solidjs.com
|
||||||
|
</a>{" "}
|
||||||
|
to learn how to build SolidStart apps.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/routes/about.tsx
Normal file
10
src/routes/about.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Title } from "@solidjs/meta";
|
||||||
|
|
||||||
|
export default function About() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Title>About</Title>
|
||||||
|
<h1>About</h1>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/routes/api/trpc/[trpc].ts
Normal file
19
src/routes/api/trpc/[trpc].ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { APIEvent } from "@solidjs/start/server";
|
||||||
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||||
|
import { appRouter } from "~/server/api/root";
|
||||||
|
|
||||||
|
const handler = (event: APIEvent) =>
|
||||||
|
// adapts tRPC to fetch API style requests
|
||||||
|
fetchRequestHandler({
|
||||||
|
// the endpoint handling the requests
|
||||||
|
endpoint: "/api/trpc",
|
||||||
|
// the request object
|
||||||
|
req: event.request,
|
||||||
|
// the router for handling the requests
|
||||||
|
router: appRouter,
|
||||||
|
// any arbitrary data that should be available to all actions
|
||||||
|
createContext: () => event
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GET = handler;
|
||||||
|
export const POST = handler;
|
||||||
13
src/routes/index.tsx
Normal file
13
src/routes/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Typewriter } from "~/components/Typewriter";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<Typewriter speed={100} delay={2000}>
|
||||||
|
<main class="text-center mx-auto text-subtext0 p-4">
|
||||||
|
{/* fill in a ipsum lorem */}
|
||||||
|
ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem
|
||||||
|
ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem
|
||||||
|
</main>
|
||||||
|
</Typewriter>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/server/api/root.ts
Normal file
16
src/server/api/root.ts
Normal 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;
|
||||||
34
src/server/api/routers/auth.ts
Normal file
34
src/server/api/routers/auth.ts
Normal 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}` };
|
||||||
|
}),
|
||||||
|
});
|
||||||
101
src/server/api/routers/database.ts
Normal file
101
src/server/api/routers/database.ts
Normal 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 };
|
||||||
|
}),
|
||||||
|
});
|
||||||
11
src/server/api/routers/example.ts
Normal file
11
src/server/api/routers/example.ts
Normal 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}!`;
|
||||||
|
})
|
||||||
|
});
|
||||||
124
src/server/api/routers/lineage.ts
Normal file
124
src/server/api/routers/lineage.ts
Normal 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" };
|
||||||
|
}),
|
||||||
|
});
|
||||||
34
src/server/api/routers/misc.ts
Normal file
34
src/server/api/routers/misc.ts
Normal 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
6
src/server/api/utils.ts
Normal 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
249
src/server/utils.ts
Normal 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;
|
||||||
|
}
|
||||||
9
src/utils.ts
Normal file
9
src/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function ConnectionFactory() {
|
||||||
|
const config = {
|
||||||
|
url: env.TURSO_DB_URL,
|
||||||
|
authToken: env.TURSO_DB_TOKEN,
|
||||||
|
};
|
||||||
|
|
||||||
|
const conn = createClient(config);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["vinxi/types/client"],
|
||||||
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user