feat(auth): Add optional authentication system (#22)
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Backend: - Add User model with username, hashed password, role, is_active - Add passlib[bcrypt] and python-jose[cryptography] dependencies - Create auth.py with JWT utilities, password hashing, role checking - Add /auth endpoints: status, setup, login, me, users CRUD - Add role-based permission system (viewer/operator/editor/admin) - Environment: SPOOLMAN_AUTH_ENABLED, SPOOLMAN_AUTH_SECRET_KEY - Migration for user table Frontend: - Add AuthProvider context with login/logout/setup - Add LoginPage component with setup mode for first user - Add ProtectedRoute wrapper for auth-required pages - Add axios interceptor to attach JWT token to requests - Add User Management tab in Settings (admin only) - Add translation keys for auth UI When SPOOLMAN_AUTH_ENABLED=true: - Users must login to access the app - First visit shows setup page to create admin account - Role-based access control for future endpoint protection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
98
client/src/pages/login/index.tsx
Normal file
98
client/src/pages/login/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { LockOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Form, Input, message, Typography } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAuth } from "../../contexts/auth";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface LoginForm {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const LoginPage: React.FC = () => {
|
||||
const { login, setup, authStatus, refreshAuthStatus } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSetupMode, setIsSetupMode] = useState(false);
|
||||
|
||||
const needsSetup = authStatus?.enabled && !authStatus?.has_users;
|
||||
|
||||
const handleSubmit = async (values: LoginForm) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (needsSetup || isSetupMode) {
|
||||
await setup(values.username, values.password);
|
||||
message.success("Admin account created successfully!");
|
||||
} else {
|
||||
await login(values.username, values.password);
|
||||
message.success("Logged in successfully!");
|
||||
}
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "Authentication failed");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "100vh",
|
||||
background: "#f0f2f5",
|
||||
}}
|
||||
>
|
||||
<Card style={{ width: 400, boxShadow: "0 4px 12px rgba(0,0,0,0.1)" }}>
|
||||
<div style={{ textAlign: "center", marginBottom: 24 }}>
|
||||
<Title level={2} style={{ marginBottom: 8 }}>
|
||||
Spoolman
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
{needsSetup || isSetupMode ? "Create Admin Account" : "Sign in to continue"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form<LoginForm> name="login" onFinish={handleSubmit} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter your username" },
|
||||
{ min: 3, message: "Username must be at least 3 characters" },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="Username" size="large" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter your password" },
|
||||
{ min: 6, message: "Password must be at least 6 characters" },
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="Password" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 12 }}>
|
||||
<Button type="primary" htmlType="submit" size="large" block loading={isLoading}>
|
||||
{needsSetup || isSetupMode ? "Create Account" : "Sign In"}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{needsSetup && (
|
||||
<Text type="secondary" style={{ display: "block", textAlign: "center" }}>
|
||||
This is the first time setup. Create an admin account to get started.
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
Reference in New Issue
Block a user