Initial commit

This commit is contained in:
root
2026-01-26 08:34:48 +01:00
commit 387314581f
216 changed files with 81204 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, users } from '@/db';
import { eq } from 'drizzle-orm';
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const handle = searchParams.get('handle')?.toLowerCase().trim();
if (!handle || handle.length < 3) {
return NextResponse.json({ available: false, error: 'Handle too short' });
}
if (!/^[a-zA-Z0-9_]+$/.test(handle)) {
return NextResponse.json({ available: false, error: 'Invalid characters' });
}
const existingUser = await db.query.users.findFirst({
where: eq(users.handle, handle),
});
return NextResponse.json({
available: !existingUser,
handle
});
} catch (error) {
console.error('Check handle error:', error);
return NextResponse.json({ error: 'Failed to check handle' }, { status: 500 });
}
}
+43
View File
@@ -0,0 +1,43 @@
import { NextResponse } from 'next/server';
import { authenticateUser, createSession } from '@/lib/auth';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const data = loginSchema.parse(body);
const user = await authenticateUser(data.email, data.password);
// Create session
await createSession(user.id);
return NextResponse.json({
success: true,
user: {
id: user.id,
handle: user.handle,
displayName: user.displayName,
},
});
} catch (error) {
console.error('Login error:', error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Login failed' },
{ status: 401 }
);
}
}
+16
View File
@@ -0,0 +1,16 @@
import { NextResponse } from 'next/server';
import { destroySession } from '@/lib/auth';
export async function POST() {
try {
await destroySession();
return NextResponse.json({ success: true });
} catch (error) {
console.error('Logout error:', error);
return NextResponse.json(
{ error: 'Logout failed' },
{ status: 500 }
);
}
}
+119
View File
@@ -0,0 +1,119 @@
import { NextResponse } from 'next/server';
import { getSession, requireAuth } from '@/lib/auth';
import { db, users } from '@/db';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
const updateProfileSchema = z.object({
displayName: z.string().min(1).max(50).optional(),
bio: z.string().max(160).optional().nullable(),
avatarUrl: z.string().url().or(z.string().length(0)).optional().nullable(),
headerUrl: z.string().url().or(z.string().length(0)).optional().nullable(),
website: z.string().url().or(z.string().length(0)).optional().nullable(),
});
export async function GET() {
try {
// Return null user if no database is connected (for UI testing)
if (!db) {
return NextResponse.json({ user: null });
}
const session = await getSession();
if (!session) {
return NextResponse.json({ user: null });
}
return NextResponse.json({
user: {
id: session.user.id,
handle: session.user.handle,
displayName: session.user.displayName,
avatarUrl: session.user.avatarUrl,
bio: session.user.bio,
website: session.user.website,
},
});
} catch (error) {
console.error('Session check error:', error);
return NextResponse.json({ user: null });
}
}
export async function PATCH(request: Request) {
try {
if (!db) {
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
}
const currentUser = await requireAuth();
const body = await request.json();
const data = updateProfileSchema.parse(body);
const updateData: {
displayName?: string;
bio?: string | null;
avatarUrl?: string | null;
headerUrl?: string | null;
website?: string | null;
updatedAt?: Date;
} = {};
if (data.displayName !== undefined) updateData.displayName = data.displayName;
if (data.bio !== undefined) updateData.bio = data.bio === '' ? null : data.bio;
if (data.avatarUrl !== undefined) updateData.avatarUrl = data.avatarUrl === '' ? null : data.avatarUrl;
if (data.headerUrl !== undefined) updateData.headerUrl = data.headerUrl === '' ? null : data.headerUrl;
if (data.website !== undefined) updateData.website = data.website === '' ? null : data.website;
if (Object.keys(updateData).length === 0) {
return NextResponse.json({
user: {
id: currentUser.id,
handle: currentUser.handle,
displayName: currentUser.displayName,
avatarUrl: currentUser.avatarUrl,
bio: currentUser.bio,
headerUrl: currentUser.headerUrl,
website: currentUser.website,
followersCount: currentUser.followersCount,
followingCount: currentUser.followingCount,
postsCount: currentUser.postsCount,
createdAt: currentUser.createdAt,
},
});
}
updateData.updatedAt = new Date();
const [updatedUser] = await db.update(users)
.set(updateData)
.where(eq(users.id, currentUser.id))
.returning();
return NextResponse.json({
user: {
id: updatedUser.id,
handle: updatedUser.handle,
displayName: updatedUser.displayName,
avatarUrl: updatedUser.avatarUrl,
bio: updatedUser.bio,
headerUrl: updatedUser.headerUrl,
website: updatedUser.website,
followersCount: updatedUser.followersCount,
followingCount: updatedUser.followingCount,
postsCount: updatedUser.postsCount,
createdAt: updatedUser.createdAt,
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid input', details: error.issues }, { status: 400 });
}
if (error instanceof Error && error.message === 'Authentication required') {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
console.error('Profile update error:', error);
return NextResponse.json({ error: 'Failed to update profile' }, { status: 500 });
}
}
+68
View File
@@ -0,0 +1,68 @@
import { NextResponse } from 'next/server';
import { registerUser, createSession } from '@/lib/auth';
import { db, nodes, users } from '@/db';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
const registerSchema = z.object({
handle: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email(),
password: z.string().min(8),
displayName: z.string().optional(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const data = registerSchema.parse(body);
const user = await registerUser(
data.handle,
data.email,
data.password,
data.displayName
);
// Check if this is an NSFW node and auto-enable NSFW settings
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const node = await db.query.nodes.findFirst({
where: eq(nodes.domain, nodeDomain),
});
if (node?.isNsfw) {
// Auto-enable NSFW viewing and mark account as NSFW for users on NSFW nodes
await db.update(users)
.set({
nsfwEnabled: true,
isNsfw: true
})
.where(eq(users.id, user.id));
}
// Create session for new user
await createSession(user.id);
return NextResponse.json({
success: true,
user: {
id: user.id,
handle: user.handle,
displayName: user.displayName,
},
});
} catch (error) {
console.error('Registration error:', error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Registration failed' },
{ status: 400 }
);
}
}