mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +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 (
|
return (
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@@ -116,7 +131,7 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Routing Preferences */}
|
{/* Security */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||||
@@ -124,52 +139,78 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold">Security</h3>
|
<h3 className="text-lg font-semibold">Security</h3>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm font-medium">Current Password</label>
|
<div>
|
||||||
<Input
|
<p className="font-medium">Require login</p>
|
||||||
type="password"
|
<p className="text-sm text-text-muted">
|
||||||
placeholder="Enter current password"
|
When ON, dashboard requires password. When OFF, access without login.
|
||||||
value={passwords.current}
|
</p>
|
||||||
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
|
</div>
|
||||||
required
|
<Toggle
|
||||||
|
checked={settings.requireLogin === true}
|
||||||
|
onChange={() => updateRequireLogin(!settings.requireLogin)}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{settings.requireLogin === true && (
|
||||||
<div className="flex flex-col gap-2">
|
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4 pt-4 border-t border-border/50">
|
||||||
<label className="text-sm font-medium">New Password</label>
|
{settings.hasPassword && (
|
||||||
<Input
|
<div className="flex flex-col gap-2">
|
||||||
type="password"
|
<label className="text-sm font-medium">Current Password</label>
|
||||||
placeholder="Enter new password"
|
<Input
|
||||||
value={passwords.new}
|
type="password"
|
||||||
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
|
placeholder="Enter current password"
|
||||||
required
|
value={passwords.current}
|
||||||
/>
|
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
|
||||||
</div>
|
required
|
||||||
<div className="flex flex-col gap-2">
|
/>
|
||||||
<label className="text-sm font-medium">Confirm New Password</label>
|
</div>
|
||||||
<Input
|
)}
|
||||||
type="password"
|
{/* {!settings.hasPassword && (
|
||||||
placeholder="Confirm new password"
|
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
||||||
value={passwords.confirm}
|
<p className="text-sm text-blue-600 dark:text-blue-400">
|
||||||
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
|
Setting password for the first time. Leave current password empty or use default: <code className="bg-blue-500/20 px-1 rounded">123456</code>
|
||||||
required
|
</p>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)} */}
|
||||||
</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 && (
|
{passStatus.message && (
|
||||||
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
||||||
{passStatus.message}
|
{passStatus.message}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<Button type="submit" variant="primary" loading={passLoading}>
|
||||||
|
{settings.hasPassword ? "Update Password" : "Set Password"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<div className="pt-2">
|
|
||||||
<Button type="submit" variant="primary" loading={passLoading}>
|
|
||||||
Update Password
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Routing Preferences */}
|
{/* Routing Preferences */}
|
||||||
|
|||||||
@@ -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() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
// Don't return the password hash to the client
|
|
||||||
const { password, ...safeSettings } = settings;
|
const { password, ...safeSettings } = settings;
|
||||||
|
|
||||||
// Add ENABLE_REQUEST_LOGS from env
|
|
||||||
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
|
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
|
||||||
|
|
||||||
return NextResponse.json({ ...safeSettings, enableRequestLogs });
|
return NextResponse.json({
|
||||||
|
...safeSettings,
|
||||||
|
enableRequestLogs,
|
||||||
|
hasPassword: !!password
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error getting settings:", error);
|
console.log("Error getting settings:", error);
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
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 });
|
return NextResponse.json({ error: "Invalid current password" }, { status: 401 });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// First time setting password, check if it matches default 123456
|
// First time setting password, no current password needed
|
||||||
if (body.currentPassword !== "123456") {
|
// Allow empty currentPassword or default "123456"
|
||||||
|
if (body.currentPassword && body.currentPassword !== "123456") {
|
||||||
return NextResponse.json({ error: "Invalid current password" }, { status: 401 });
|
return NextResponse.json({ error: "Invalid current password" }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ export default function LoginPage() {
|
|||||||
const [hasPassword, setHasPassword] = useState(null);
|
const [hasPassword, setHasPassword] = useState(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Check if password is set on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function checkPassword() {
|
async function checkAuth() {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
||||||
const res = await fetch(`${baseUrl}/api/settings`, {
|
const res = await fetch(`${baseUrl}/api/settings`, {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
@@ -26,8 +25,12 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
if (data.requireLogin === false) {
|
||||||
|
router.push("/dashboard");
|
||||||
|
router.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!data.password) {
|
if (!data.password) {
|
||||||
// No password set - auto login
|
|
||||||
const loginRes = await fetch(`${baseUrl}/api/auth/login`, {
|
const loginRes = await fetch(`${baseUrl}/api/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -43,11 +46,10 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
// Silent fail - default to showing login form
|
|
||||||
setHasPassword(true);
|
setHasPassword(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkPassword();
|
checkAuth();
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e) => {
|
||||||
|
|||||||
+2
-1
@@ -48,7 +48,8 @@ const defaultData = {
|
|||||||
apiKeys: [],
|
apiKeys: [],
|
||||||
settings: {
|
settings: {
|
||||||
cloudEnabled: false,
|
cloudEnabled: false,
|
||||||
stickyRoundRobinLimit: 3
|
stickyRoundRobinLimit: 3,
|
||||||
|
requireLogin: true
|
||||||
},
|
},
|
||||||
pricing: {} // NEW: pricing configuration
|
pricing: {} // NEW: pricing configuration
|
||||||
};
|
};
|
||||||
|
|||||||
+15
-5
@@ -12,16 +12,26 @@ export async function proxy(request) {
|
|||||||
if (pathname.startsWith("/dashboard")) {
|
if (pathname.startsWith("/dashboard")) {
|
||||||
const token = request.cookies.get("auth_token")?.value;
|
const token = request.cookies.get("auth_token")?.value;
|
||||||
|
|
||||||
if (!token) {
|
if (token) {
|
||||||
return NextResponse.redirect(new URL("/login", request.url));
|
try {
|
||||||
|
await jwtVerify(token, SECRET);
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (err) {
|
||||||
|
return NextResponse.redirect(new URL("/login", request.url));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const origin = request.nextUrl.origin;
|
||||||
try {
|
try {
|
||||||
await jwtVerify(token, SECRET);
|
const res = await fetch(`${origin}/api/settings/require-login`);
|
||||||
return NextResponse.next();
|
const data = await res.json();
|
||||||
|
if (data.requireLogin === false) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
} catch (err) {
|
} 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
|
// Redirect / to /dashboard if logged in, or /dashboard if it's the root
|
||||||
|
|||||||
Reference in New Issue
Block a user