add initial complete webui, more ai commands for moderation, add api
This commit is contained in:
parent
19e794e34c
commit
173d4e7a52
112 changed files with 8176 additions and 780 deletions
34
webui/app/api/auth/logout/route.ts
Executable file
34
webui/app/api/auth/logout/route.ts
Executable file
|
@ -0,0 +1,34 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { invalidateSession } from "@/lib/auth";
|
||||
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
const sessionToken = bearerToken || cookieToken;
|
||||
|
||||
if (sessionToken) {
|
||||
await invalidateSession(sessionToken);
|
||||
}
|
||||
|
||||
const response = NextResponse.json({ success: true });
|
||||
|
||||
response.cookies.set(SESSION_COOKIE_NAME, '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
expires: new Date(0),
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in logout API:", error);
|
||||
return NextResponse.json({
|
||||
error: "Internal server error"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
91
webui/app/api/auth/username/route.ts
Executable file
91
webui/app/api/auth/username/route.ts
Executable file
|
@ -0,0 +1,91 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as schema from "@/lib/schema";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const requestContentType = request.headers.get('content-type');
|
||||
if (!requestContentType || !requestContentType.includes('application/json')) {
|
||||
return NextResponse.json({ success: false, error: "Invalid content type" }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { username } = body;
|
||||
|
||||
if (!username) {
|
||||
return NextResponse.json({ success: false, error: "Username is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof username !== 'string' || username.length < 3 || username.length > 32) {
|
||||
return NextResponse.json({ success: false, error: "Invalid username format" }, { status: 400 });
|
||||
}
|
||||
|
||||
const cleanUsername = username.replace('@', '');
|
||||
|
||||
const user = await db.query.usersTable.findFirst({
|
||||
where: eq(schema.usersTable.username, cleanUsername),
|
||||
columns: {
|
||||
telegramId: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
const botUsername = process.env.botUsername || "KowalskiNodeBot";
|
||||
return NextResponse.json({ success: false, error: `Please DM @${botUsername} before signing in.` }, { status: 404 });
|
||||
}
|
||||
|
||||
const botApiUrl = process.env.botApiUrl || "http://kowalski:3030";
|
||||
const fullUrl = `${botApiUrl}/2fa/get`;
|
||||
|
||||
const botApiResponse = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ userId: user.telegramId }),
|
||||
});
|
||||
|
||||
if (!botApiResponse.ok) {
|
||||
const errorText = await botApiResponse.text();
|
||||
console.error("Bot API error response:", errorText);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `Bot API error: ${botApiResponse.status} - ${errorText.slice(0, 200)}`
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
const contentType = botApiResponse.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
const errorText = await botApiResponse.text();
|
||||
console.error("Bot API returned non-JSON:", errorText.slice(0, 200));
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Bot API returned invalid response format"
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
const botApiResult = await botApiResponse.json();
|
||||
|
||||
if (!botApiResult.generated) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: botApiResult.error || "Failed to send 2FA code"
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "2FA code sent successfully",
|
||||
userId: user.telegramId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in username API:", error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Internal server error"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
107
webui/app/api/auth/verify/route.ts
Executable file
107
webui/app/api/auth/verify/route.ts
Executable file
|
@ -0,0 +1,107 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { eq, and, gt } from "drizzle-orm";
|
||||
import * as schema from "@/lib/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import { createSession, getSessionCookieOptions } from "@/lib/auth";
|
||||
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const contentType = request.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Invalid content type"
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { userId, code } = body;
|
||||
|
||||
if (!userId || !code) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "User ID and code are required"
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof userId !== 'string' || typeof code !== 'string') {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Invalid input format"
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Invalid code format"
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const twoFactorRecord = await db.query.twoFactorTable.findFirst({
|
||||
where: and(
|
||||
eq(schema.twoFactorTable.userId, userId),
|
||||
gt(schema.twoFactorTable.codeExpiresAt, new Date())
|
||||
),
|
||||
});
|
||||
|
||||
if (!twoFactorRecord) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "No valid 2FA code found or code has expired"
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
if (twoFactorRecord.codeAttempts >= 5) {
|
||||
await db.delete(schema.twoFactorTable)
|
||||
.where(eq(schema.twoFactorTable.userId, userId));
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Too many failed attempts. Please request a new code."
|
||||
}, { status: 429 });
|
||||
}
|
||||
|
||||
if (twoFactorRecord.currentCode !== code) {
|
||||
await db.update(schema.twoFactorTable)
|
||||
.set({
|
||||
codeAttempts: twoFactorRecord.codeAttempts + 1,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(schema.twoFactorTable.userId, userId));
|
||||
|
||||
console.log(`2FA verification failed for user: ${userId}, attempts: ${twoFactorRecord.codeAttempts + 1}`);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Invalid 2FA code"
|
||||
}, { status: 401 });
|
||||
}
|
||||
|
||||
const session = await createSession(userId);
|
||||
|
||||
await db.delete(schema.twoFactorTable)
|
||||
.where(eq(schema.twoFactorTable.userId, userId));
|
||||
|
||||
console.log("2FA verification successful for user:", userId);
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
message: "2FA verification successful",
|
||||
redirectTo: "/account",
|
||||
sessionToken: session.sessionToken
|
||||
});
|
||||
|
||||
const cookieOptions = getSessionCookieOptions();
|
||||
response.cookies.set(SESSION_COOKIE_NAME, session.sessionToken, cookieOptions);
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in verify API:", error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Internal server error"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
59
webui/app/api/user/delete/route.ts
Executable file
59
webui/app/api/user/delete/route.ts
Executable file
|
@ -0,0 +1,59 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { validateSession } from "@/lib/auth";
|
||||
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { usersTable, sessionsTable, twoFactorTable } from "@/lib/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
const sessionToken = bearerToken || cookieToken;
|
||||
|
||||
if (!sessionToken) {
|
||||
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await validateSession(sessionToken);
|
||||
|
||||
if (!sessionData || !sessionData.user) {
|
||||
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = sessionData.user.telegramId;
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(sessionsTable)
|
||||
.where(eq(sessionsTable.userId, userId));
|
||||
|
||||
await tx.delete(twoFactorTable)
|
||||
.where(eq(twoFactorTable.userId, userId));
|
||||
|
||||
await tx.delete(usersTable)
|
||||
.where(eq(usersTable.telegramId, userId));
|
||||
});
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
message: "Account deleted successfully"
|
||||
});
|
||||
|
||||
response.cookies.set(SESSION_COOKIE_NAME, '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
expires: new Date(0),
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting account:", error);
|
||||
return NextResponse.json({
|
||||
error: "Failed to delete account"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
46
webui/app/api/user/profile/route.ts
Executable file
46
webui/app/api/user/profile/route.ts
Executable file
|
@ -0,0 +1,46 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { validateSession } from "@/lib/auth";
|
||||
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
const sessionToken = bearerToken || cookieToken;
|
||||
|
||||
if (!sessionToken) {
|
||||
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await validateSession(sessionToken);
|
||||
|
||||
if (!sessionData || !sessionData.user) {
|
||||
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { user } = sessionData;
|
||||
const sanitizedUser = {
|
||||
telegramId: user.telegramId,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
aiEnabled: user.aiEnabled,
|
||||
showThinking: user.showThinking,
|
||||
customAiModel: user.customAiModel,
|
||||
aiTemperature: user.aiTemperature,
|
||||
aiRequests: user.aiRequests,
|
||||
aiCharacters: user.aiCharacters,
|
||||
disabledCommands: user.disabledCommands,
|
||||
languageCode: user.languageCode,
|
||||
};
|
||||
|
||||
return NextResponse.json(sanitizedUser);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in profile API:", error);
|
||||
return NextResponse.json({
|
||||
error: "Internal server error"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
103
webui/app/api/user/settings/route.ts
Executable file
103
webui/app/api/user/settings/route.ts
Executable file
|
@ -0,0 +1,103 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { validateSession } from "@/lib/auth";
|
||||
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
|
||||
import { db } from "@/lib/db";
|
||||
import * as schema from "@/lib/schema";
|
||||
|
||||
interface UserUpdates {
|
||||
aiEnabled?: boolean;
|
||||
showThinking?: boolean;
|
||||
customAiModel?: string;
|
||||
aiTemperature?: number;
|
||||
disabledCommands?: string[];
|
||||
languageCode?: string;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
const sessionToken = bearerToken || cookieToken;
|
||||
|
||||
if (!sessionToken) {
|
||||
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await validateSession(sessionToken);
|
||||
|
||||
if (!sessionData || !sessionData.user) {
|
||||
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
|
||||
}
|
||||
|
||||
const contentType = request.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
return NextResponse.json({ error: "Invalid content type" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updates = await request.json();
|
||||
const userId = sessionData.user.telegramId;
|
||||
|
||||
if (!updates || typeof updates !== 'object') {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const allowedFields = [
|
||||
'aiEnabled',
|
||||
'showThinking',
|
||||
'customAiModel',
|
||||
'aiTemperature',
|
||||
'disabledCommands',
|
||||
'languageCode'
|
||||
];
|
||||
|
||||
const filteredUpdates: UserUpdates = {};
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (allowedFields.includes(key)) {
|
||||
if (key === 'aiEnabled' || key === 'showThinking') {
|
||||
filteredUpdates[key] = Boolean(value);
|
||||
} else if (key === 'aiTemperature') {
|
||||
const temp = Number(value);
|
||||
if (temp >= 0.1 && temp <= 2.0) {
|
||||
filteredUpdates[key] = temp;
|
||||
} else {
|
||||
return NextResponse.json({ error: "Temperature must be between 0.1 and 2.0" }, { status: 400 });
|
||||
}
|
||||
} else if (key === 'customAiModel' || key === 'languageCode') {
|
||||
if (typeof value === 'string' && value.length > 0 && value.length < 100) {
|
||||
filteredUpdates[key] = value;
|
||||
} else {
|
||||
return NextResponse.json({ error: `Invalid ${key}` }, { status: 400 });
|
||||
}
|
||||
} else if (key === 'disabledCommands') {
|
||||
if (Array.isArray(value) && value.every(item => typeof item === 'string' && item.length < 50) && value.length < 100) {
|
||||
filteredUpdates[key] = value;
|
||||
} else {
|
||||
return NextResponse.json({ error: "Invalid disabled commands" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(filteredUpdates).length === 0) {
|
||||
return NextResponse.json({ error: "No valid updates provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
filteredUpdates.updatedAt = new Date();
|
||||
|
||||
await db.update(schema.usersTable)
|
||||
.set(filteredUpdates)
|
||||
.where(eq(schema.usersTable.telegramId, userId));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in settings API:", error);
|
||||
return NextResponse.json({
|
||||
error: "Internal server error"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue