273 lines
8.1 KiB
TypeScript
273 lines
8.1 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
// JWT utilities
|
|
async function signJWT(payload: any, secret: string): Promise<string> {
|
|
const header = {
|
|
alg: 'HS256',
|
|
typ: 'JWT'
|
|
};
|
|
|
|
const encodedHeader = btoa(JSON.stringify(header));
|
|
const encodedPayload = btoa(JSON.stringify(payload));
|
|
|
|
const data = `${encodedHeader}.${encodedPayload}`;
|
|
const signature = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(secret),
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign']
|
|
);
|
|
|
|
const signatureBuffer = await crypto.subtle.sign('HMAC', signature, new TextEncoder().encode(data));
|
|
const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));
|
|
|
|
return `${data}.${encodedSignature}`;
|
|
}
|
|
|
|
async function verifyJWT(token: string, secret: string): Promise<any> {
|
|
const [header, payload, signature] = token.split('.');
|
|
|
|
const data = `${header}.${payload}`;
|
|
const signatureBuffer = new Uint8Array(
|
|
atob(signature).split('').map(c => c.charCodeAt(0))
|
|
);
|
|
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(secret),
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['verify']
|
|
);
|
|
|
|
const isValid = await crypto.subtle.verify('HMAC', key, signatureBuffer, new TextEncoder().encode(data));
|
|
|
|
if (!isValid) {
|
|
throw new Error('Invalid token');
|
|
}
|
|
|
|
return JSON.parse(atob(payload));
|
|
}
|
|
|
|
// Password hashing
|
|
async function hashPassword(password: string): Promise<string> {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(password);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
async function comparePasswords(password: string, hashedPassword: string): Promise<boolean> {
|
|
const hashedInput = await hashPassword(password);
|
|
return hashedInput === hashedPassword;
|
|
}
|
|
|
|
// Validation schemas
|
|
const registerSchema = z.object({
|
|
username: z.string().min(3).max(50),
|
|
email: z.string().email(),
|
|
password: z.string().min(8),
|
|
fullName: z.string().optional()
|
|
});
|
|
|
|
const loginSchema = z.object({
|
|
username: z.string(),
|
|
password: z.string()
|
|
});
|
|
|
|
// User interface
|
|
interface User {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
password: string;
|
|
fullName?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export const onRequest: PagesFunction = async (context) => {
|
|
const { request, env } = context;
|
|
const url = new URL(request.url);
|
|
const path = url.pathname.split('/').pop();
|
|
|
|
const JWT_SECRET = env.JWT_SECRET || 'pilateswithfadia-secret-key';
|
|
|
|
// Helper function to get user from KV
|
|
async function getUserByUsername(username: string): Promise<User | null> {
|
|
const userData = await env.STORAGE.get(`user:${username}`);
|
|
return userData ? JSON.parse(userData) : null;
|
|
}
|
|
|
|
async function getUserByEmail(email: string): Promise<User | null> {
|
|
const userData = await env.STORAGE.get(`user_email:${email}`);
|
|
return userData ? JSON.parse(userData) : null;
|
|
}
|
|
|
|
async function createUser(userData: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
const id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const user: User = {
|
|
...userData,
|
|
id,
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
|
|
await env.STORAGE.put(`user:${user.username}`, JSON.stringify(user));
|
|
await env.STORAGE.put(`user_email:${user.email}`, JSON.stringify(user));
|
|
|
|
return user;
|
|
}
|
|
|
|
// Register endpoint
|
|
if (path === 'register' && request.method === 'POST') {
|
|
try {
|
|
const body = await request.json();
|
|
const { username, email, password, fullName } = registerSchema.parse(body);
|
|
|
|
// Check if username already exists
|
|
const existingUserByUsername = await getUserByUsername(username);
|
|
if (existingUserByUsername) {
|
|
return new Response(JSON.stringify({ message: "Username already exists" }), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
// Check if email already exists
|
|
const existingUserByEmail = await getUserByEmail(email);
|
|
if (existingUserByEmail) {
|
|
return new Response(JSON.stringify({ message: "Email already in use" }), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
const hashedPassword = await hashPassword(password);
|
|
const user = await createUser({
|
|
username,
|
|
email,
|
|
password: hashedPassword,
|
|
fullName
|
|
});
|
|
|
|
const token = await signJWT(
|
|
{ userId: user.id, username: user.username },
|
|
JWT_SECRET
|
|
);
|
|
|
|
// Return user without password
|
|
const { password: _, ...userWithoutPassword } = user;
|
|
|
|
return new Response(JSON.stringify({
|
|
user: userWithoutPassword,
|
|
token
|
|
}), {
|
|
status: 201,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return new Response(JSON.stringify({
|
|
message: "Invalid registration data",
|
|
errors: error.errors
|
|
}), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ message: "Registration failed" }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
}
|
|
|
|
// Login endpoint
|
|
if (path === 'login' && request.method === 'POST') {
|
|
try {
|
|
const body = await request.json();
|
|
const { username, password } = loginSchema.parse(body);
|
|
|
|
const user = await getUserByUsername(username);
|
|
if (!user || !(await comparePasswords(password, user.password))) {
|
|
return new Response(JSON.stringify({ message: "Invalid username or password" }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
const token = await signJWT(
|
|
{ userId: user.id, username: user.username },
|
|
JWT_SECRET
|
|
);
|
|
|
|
// Return user without password
|
|
const { password: _, ...userWithoutPassword } = user;
|
|
|
|
return new Response(JSON.stringify({
|
|
user: userWithoutPassword,
|
|
token
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return new Response(JSON.stringify({
|
|
message: "Invalid login data",
|
|
errors: error.errors
|
|
}), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ message: "Login failed" }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
}
|
|
|
|
// Me endpoint (get current user)
|
|
if (path === 'me' && request.method === 'GET') {
|
|
try {
|
|
const authHeader = request.headers.get('Authorization');
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return new Response(JSON.stringify({ message: "Not authenticated" }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
const payload = await verifyJWT(token, JWT_SECRET);
|
|
|
|
const user = await getUserByUsername(payload.username);
|
|
if (!user) {
|
|
return new Response(JSON.stringify({ message: "User not found" }), {
|
|
status: 404,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
// Return user without password
|
|
const { password: _, ...userWithoutPassword } = user;
|
|
|
|
return new Response(JSON.stringify(userWithoutPassword), {
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
} catch (error) {
|
|
return new Response(JSON.stringify({ message: "Invalid token" }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
}
|
|
|
|
return new Response(JSON.stringify({ message: "Not found" }), {
|
|
status: 404,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
};
|