mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
feat(auth): Enhance authentication flow and settings management
This commit is contained in:
@@ -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 */}
|
||||
|
||||
12
src/app/api/settings/require-login/route.js
Normal file
12
src/app/api/settings/require-login/route.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -48,7 +48,8 @@ const defaultData = {
|
||||
apiKeys: [],
|
||||
settings: {
|
||||
cloudEnabled: false,
|
||||
stickyRoundRobinLimit: 3
|
||||
stickyRoundRobinLimit: 3,
|
||||
requireLogin: true
|
||||
},
|
||||
pricing: {} // NEW: pricing configuration
|
||||
};
|
||||
|
||||
20
src/proxy.js
20
src/proxy.js
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user