Youngger9765 Claude commited on
Commit
4b54973
·
1 Parent(s): 3f45635

Add missing password protection files

Browse files

- Add PasswordProtection component (tsx and css)
- Add password_auth middleware
- Fix HF Spaces build error

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

backend/app/middleware/password_auth.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, status
2
+ from fastapi.responses import JSONResponse
3
+ from starlette.middleware.base import BaseHTTPMiddleware
4
+ from app.config.settings import get_settings
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class PasswordProtectionMiddleware(BaseHTTPMiddleware):
10
+ """
11
+ Middleware to protect API endpoints with password verification
12
+ """
13
+
14
+ def __init__(self, app):
15
+ super().__init__(app)
16
+ self.settings = get_settings()
17
+
18
+ # Endpoints that don't require password protection
19
+ self.exempt_paths = {
20
+ "/api/verify-password",
21
+ "/health",
22
+ "/api/health",
23
+ "/docs",
24
+ "/openapi.json",
25
+ "/redoc"
26
+ }
27
+
28
+ async def dispatch(self, request: Request, call_next):
29
+ # Skip protection for exempt paths
30
+ if request.url.path in self.exempt_paths:
31
+ return await call_next(request)
32
+
33
+ # Skip protection for non-API paths (static files, frontend)
34
+ if not request.url.path.startswith("/api/"):
35
+ return await call_next(request)
36
+
37
+ # Skip protection for OPTIONS requests (CORS preflight)
38
+ if request.method == "OPTIONS":
39
+ return await call_next(request)
40
+
41
+ # Check for password verification in session/headers
42
+ password_verified = request.headers.get("X-Password-Verified")
43
+
44
+ if password_verified != "true":
45
+ return JSONResponse(
46
+ status_code=status.HTTP_401_UNAUTHORIZED,
47
+ content={
48
+ "success": False,
49
+ "error": {
50
+ "code": "PASSWORD_REQUIRED",
51
+ "message": "Password verification required to access this endpoint"
52
+ }
53
+ }
54
+ )
55
+
56
+ return await call_next(request)
frontend/src/components/PasswordProtection.css ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .password-protection {
2
+ min-height: 100vh;
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
7
+ padding: 20px;
8
+ }
9
+
10
+ .password-container {
11
+ background: white;
12
+ border-radius: 16px;
13
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
14
+ padding: 40px;
15
+ width: 100%;
16
+ max-width: 400px;
17
+ text-align: center;
18
+ }
19
+
20
+ .password-header {
21
+ margin-bottom: 30px;
22
+ }
23
+
24
+ .password-header h1 {
25
+ color: #2c3e50;
26
+ font-size: 2rem;
27
+ margin-bottom: 10px;
28
+ font-weight: 700;
29
+ }
30
+
31
+ .password-header p {
32
+ color: #7f8c8d;
33
+ font-size: 1rem;
34
+ margin: 0;
35
+ }
36
+
37
+ .password-form {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 20px;
41
+ }
42
+
43
+ .password-input-group {
44
+ text-align: left;
45
+ }
46
+
47
+ .password-input-group label {
48
+ display: block;
49
+ color: #2c3e50;
50
+ font-weight: 600;
51
+ margin-bottom: 8px;
52
+ }
53
+
54
+ .password-input-group input {
55
+ width: 100%;
56
+ padding: 14px 16px;
57
+ border: 2px solid #e1e8ed;
58
+ border-radius: 8px;
59
+ font-size: 1rem;
60
+ transition: all 0.3s ease;
61
+ box-sizing: border-box;
62
+ }
63
+
64
+ .password-input-group input:focus {
65
+ outline: none;
66
+ border-color: #667eea;
67
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
68
+ }
69
+
70
+ .password-input-group input:disabled {
71
+ background-color: #f8f9fa;
72
+ cursor: not-allowed;
73
+ }
74
+
75
+ .password-error {
76
+ background-color: #fee;
77
+ border: 1px solid #f5c6cb;
78
+ color: #721c24;
79
+ padding: 12px;
80
+ border-radius: 6px;
81
+ font-size: 0.9rem;
82
+ }
83
+
84
+ .password-submit {
85
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
86
+ color: white;
87
+ border: none;
88
+ padding: 14px 24px;
89
+ border-radius: 8px;
90
+ font-size: 1rem;
91
+ font-weight: 600;
92
+ cursor: pointer;
93
+ transition: all 0.3s ease;
94
+ }
95
+
96
+ .password-submit:hover:not(:disabled) {
97
+ transform: translateY(-2px);
98
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
99
+ }
100
+
101
+ .password-submit:disabled {
102
+ opacity: 0.6;
103
+ cursor: not-allowed;
104
+ transform: none;
105
+ box-shadow: none;
106
+ }
107
+
108
+ .password-footer {
109
+ margin-top: 30px;
110
+ color: #95a5a6;
111
+ }
112
+
113
+ .password-footer small {
114
+ font-size: 0.85rem;
115
+ line-height: 1.4;
116
+ }
117
+
118
+ @media (max-width: 480px) {
119
+ .password-container {
120
+ padding: 30px 20px;
121
+ margin: 20px;
122
+ }
123
+
124
+ .password-header h1 {
125
+ font-size: 1.75rem;
126
+ }
127
+ }
frontend/src/components/PasswordProtection.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import './PasswordProtection.css';
3
+ import { gradingApi } from '../api/grading';
4
+
5
+ interface PasswordProtectionProps {
6
+ onPasswordCorrect: () => void;
7
+ }
8
+
9
+ const PasswordProtection: React.FC<PasswordProtectionProps> = ({ onPasswordCorrect }) => {
10
+ const [password, setPassword] = useState('');
11
+ const [error, setError] = useState('');
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ // 檢查是否已經有有效的密碼 session
15
+ useEffect(() => {
16
+ const savedPassword = sessionStorage.getItem('app_password_verified');
17
+ if (savedPassword === 'true') {
18
+ onPasswordCorrect();
19
+ }
20
+ }, [onPasswordCorrect]);
21
+
22
+ const handleSubmit = async (e: React.FormEvent) => {
23
+ e.preventDefault();
24
+
25
+ if (!password.trim()) {
26
+ setError('請輸入密碼');
27
+ return;
28
+ }
29
+
30
+ setLoading(true);
31
+ setError('');
32
+
33
+ try {
34
+ // 發送密碼驗證請求到後端
35
+ const data = await gradingApi.verifyPassword(password);
36
+
37
+ if (data.success) {
38
+ // 密碼正確,保存到 sessionStorage
39
+ sessionStorage.setItem('app_password_verified', 'true');
40
+ onPasswordCorrect();
41
+ } else {
42
+ setError(data.message || '密碼錯誤,請重試');
43
+ }
44
+ } catch (err) {
45
+ console.error('Password verification error:', err);
46
+ setError('驗證失敗,請檢查網路連線');
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ };
51
+
52
+ return (
53
+ <div className="password-protection">
54
+ <div className="password-container">
55
+ <div className="password-header">
56
+ <h1>🔒 AI 批改平台</h1>
57
+ <p>請輸入存取密碼以繼續使用</p>
58
+ </div>
59
+
60
+ <form onSubmit={handleSubmit} className="password-form">
61
+ <div className="password-input-group">
62
+ <label htmlFor="password">存取密碼</label>
63
+ <input
64
+ type="password"
65
+ id="password"
66
+ value={password}
67
+ onChange={(e) => setPassword(e.target.value)}
68
+ placeholder="請輸入密碼"
69
+ disabled={loading}
70
+ autoFocus
71
+ />
72
+ </div>
73
+
74
+ {error && (
75
+ <div className="password-error">
76
+ {error}
77
+ </div>
78
+ )}
79
+
80
+ <button
81
+ type="submit"
82
+ className="password-submit"
83
+ disabled={loading || !password.trim()}
84
+ >
85
+ {loading ? '驗證中...' : '進入平台'}
86
+ </button>
87
+ </form>
88
+
89
+ <div className="password-footer">
90
+ <p>
91
+ <small>
92
+ 此平台需要密碼保護以確保使用安全性
93
+ <br />
94
+ 如需協助,請聯絡管理員
95
+ </small>
96
+ </p>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ );
101
+ };
102
+
103
+ export default PasswordProtection;