feat(auth): Enhance authentication flow and settings management

This commit is contained in:
decolua
2026-02-05 11:26:11 +07:00
parent 0a026c7af6
commit 249fc28c49
6 changed files with 129 additions and 60 deletions

View File

@@ -95,6 +95,21 @@ export default function ProfilePage() {
}
};
const updateRequireLogin = async (requireLogin) => {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ requireLogin }),
});
if (res.ok) {
setSettings(prev => ({ ...prev, requireLogin }));
}
} catch (err) {
console.error("Failed to update require login:", err);
}
};
return (
<div className="max-w-2xl mx-auto">
<div className="flex flex-col gap-6">
@@ -116,7 +131,7 @@ export default function ProfilePage() {
</div>
</Card>
{/* Routing Preferences */}
{/* Security */}
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-primary/10 text-primary">
@@ -124,52 +139,78 @@ export default function ProfilePage() {
</div>
<h3 className="text-lg font-semibold">Security</h3>
</div>
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Current Password</label>
<Input
type="password"
placeholder="Enter current password"
value={passwords.current}
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
required
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Require login</p>
<p className="text-sm text-text-muted">
When ON, dashboard requires password. When OFF, access without login.
</p>
</div>
<Toggle
checked={settings.requireLogin === true}
onChange={() => updateRequireLogin(!settings.requireLogin)}
disabled={loading}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">New Password</label>
<Input
type="password"
placeholder="Enter new password"
value={passwords.new}
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
required
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Confirm New Password</label>
<Input
type="password"
placeholder="Confirm new password"
value={passwords.confirm}
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
required
/>
</div>
</div>
{settings.requireLogin === true && (
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4 pt-4 border-t border-border/50">
{settings.hasPassword && (
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Current Password</label>
<Input
type="password"
placeholder="Enter current password"
value={passwords.current}
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
required
/>
</div>
)}
{/* {!settings.hasPassword && (
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
<p className="text-sm text-blue-600 dark:text-blue-400">
Setting password for the first time. Leave current password empty or use default: <code className="bg-blue-500/20 px-1 rounded">123456</code>
</p>
</div>
)} */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">New Password</label>
<Input
type="password"
placeholder="Enter new password"
value={passwords.new}
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
required
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Confirm New Password</label>
<Input
type="password"
placeholder="Confirm new password"
value={passwords.confirm}
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
required
/>
</div>
</div>
{passStatus.message && (
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
{passStatus.message}
</p>
{passStatus.message && (
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
{passStatus.message}
</p>
)}
<div className="pt-2">
<Button type="submit" variant="primary" loading={passLoading}>
{settings.hasPassword ? "Update Password" : "Set Password"}
</Button>
</div>
</form>
)}
<div className="pt-2">
<Button type="submit" variant="primary" loading={passLoading}>
Update Password
</Button>
</div>
</form>
</div>
</Card>
{/* Routing Preferences */}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { getSettings } from "@/lib/localDb";
export async function GET() {
try {
const settings = await getSettings();
const requireLogin = settings.requireLogin !== false;
return NextResponse.json({ requireLogin });
} catch (error) {
return NextResponse.json({ requireLogin: true }, { status: 200 });
}
}

View File

@@ -5,13 +5,15 @@ import bcrypt from "bcryptjs";
export async function GET() {
try {
const settings = await getSettings();
// Don't return the password hash to the client
const { password, ...safeSettings } = settings;
// Add ENABLE_REQUEST_LOGS from env
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
return NextResponse.json({ ...safeSettings, enableRequestLogs });
return NextResponse.json({
...safeSettings,
enableRequestLogs,
hasPassword: !!password
});
} catch (error) {
console.log("Error getting settings:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
@@ -37,8 +39,9 @@ export async function PATCH(request) {
return NextResponse.json({ error: "Invalid current password" }, { status: 401 });
}
} else {
// First time setting password, check if it matches default 123456
if (body.currentPassword !== "123456") {
// First time setting password, no current password needed
// Allow empty currentPassword or default "123456"
if (body.currentPassword && body.currentPassword !== "123456") {
return NextResponse.json({ error: "Invalid current password" }, { status: 401 });
}
}

View File

@@ -11,14 +11,13 @@ export default function LoginPage() {
const [hasPassword, setHasPassword] = useState(null);
const router = useRouter();
// Check if password is set on mount
useEffect(() => {
async function checkPassword() {
async function checkAuth() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
try {
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
const res = await fetch(`${baseUrl}/api/settings`, {
signal: controller.signal,
});
@@ -26,8 +25,12 @@ export default function LoginPage() {
if (res.ok) {
const data = await res.json();
if (data.requireLogin === false) {
router.push("/dashboard");
router.refresh();
return;
}
if (!data.password) {
// No password set - auto login
const loginRes = await fetch(`${baseUrl}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -43,11 +46,10 @@ export default function LoginPage() {
}
} catch (err) {
clearTimeout(timeoutId);
// Silent fail - default to showing login form
setHasPassword(true);
}
}
checkPassword();
checkAuth();
}, [router]);
const handleLogin = async (e) => {

View File

@@ -48,7 +48,8 @@ const defaultData = {
apiKeys: [],
settings: {
cloudEnabled: false,
stickyRoundRobinLimit: 3
stickyRoundRobinLimit: 3,
requireLogin: true
},
pricing: {} // NEW: pricing configuration
};

View File

@@ -12,16 +12,26 @@ export async function proxy(request) {
if (pathname.startsWith("/dashboard")) {
const token = request.cookies.get("auth_token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
if (token) {
try {
await jwtVerify(token, SECRET);
return NextResponse.next();
} catch (err) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
const origin = request.nextUrl.origin;
try {
await jwtVerify(token, SECRET);
return NextResponse.next();
const res = await fetch(`${origin}/api/settings/require-login`);
const data = await res.json();
if (data.requireLogin === false) {
return NextResponse.next();
}
} catch (err) {
return NextResponse.redirect(new URL("/login", request.url));
// On error, require login
}
return NextResponse.redirect(new URL("/login", request.url));
}
// Redirect / to /dashboard if logged in, or /dashboard if it's the root