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>
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
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;
|