All checks were successful
Deploy on Master Change / deploy (push) Successful in 39s
261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useAuth } from '../auth/AuthContext';
|
|
import { useGitHubConnection } from '../hooks/useGitHubConnection';
|
|
import { GitHubReposPanel } from './GitHubReposPanel';
|
|
import { OrganizationSection } from './OrganizationSection';
|
|
import { apiQldt } from '../api';
|
|
|
|
export function ChangePasswordPage({ forced }: { forced?: boolean }) {
|
|
const { changePassword, logout } = useAuth();
|
|
const [oldPassword, setOldPassword] = useState('');
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirm, setConfirm] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const submit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
if (newPassword.length < 8) {
|
|
setError('Mật khẩu mới tối thiểu 8 ký tự');
|
|
return;
|
|
}
|
|
if (newPassword !== confirm) {
|
|
setError('Xác nhận mật khẩu không khớp');
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
await changePassword(oldPassword, newPassword);
|
|
} catch (err: any) {
|
|
setError(err?.message || 'Đổi mật khẩu thất bại');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="login-screen">
|
|
<div className="login-card">
|
|
<h1 className="login-heading">{forced ? 'Đổi mật khẩu bắt buộc' : 'Đổi mật khẩu'}</h1>
|
|
{forced && <p className="login-hint">Lần đăng nhập đầu tiên — vui lòng đặt mật khẩu mới trước khi tiếp tục.</p>}
|
|
<form onSubmit={submit} className="login-form">
|
|
<label className="login-field">
|
|
<span>Mật khẩu hiện tại</span>
|
|
<input type="password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} required />
|
|
</label>
|
|
<label className="login-field">
|
|
<span>Mật khẩu mới</span>
|
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required minLength={8} />
|
|
</label>
|
|
<label className="login-field">
|
|
<span>Xác nhận mật khẩu mới</span>
|
|
<input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required minLength={8} />
|
|
</label>
|
|
{error && <div className="login-error">{error}</div>}
|
|
<button type="submit" className="btn btn-primary login-submit" disabled={busy}>
|
|
{busy ? 'Đang lưu...' : 'Lưu mật khẩu'}
|
|
</button>
|
|
{!forced && (
|
|
<button type="button" className="link-btn" style={{ marginTop: '0.5rem' }} onClick={logout}>
|
|
Đăng xuất
|
|
</button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function EmailDomainsTab() {
|
|
return (
|
|
<div className="tab-page">
|
|
<header className="page-header">
|
|
<h1 className="page-title">Quản lý tổ chức</h1>
|
|
</header>
|
|
<OrganizationSection />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function MyAccountTab() {
|
|
const { staff, changePassword } = useAuth();
|
|
const gh = useGitHubConnection();
|
|
const [oldPw, setOldPw] = useState('');
|
|
const [newPw, setNewPw] = useState('');
|
|
const [pwMsg, setPwMsg] = useState('');
|
|
const [pwErr, setPwErr] = useState('');
|
|
|
|
// Cấu hình QLĐT Token (Chỉ cho phuocntb@rikkeiacademy.com)
|
|
const isQldtAdmin = staff?.email === 'phuocntb@rikkeiacademy.com';
|
|
const [qldtToken, setQldtToken] = useState('');
|
|
const [qldtMsg, setQldtMsg] = useState('');
|
|
const [qldtErr, setQldtErr] = useState('');
|
|
const [loadingQldt, setLoadingQldt] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!isQldtAdmin) return;
|
|
setLoadingQldt(true);
|
|
apiQldt.getToken()
|
|
.then((res) => {
|
|
setQldtToken(res.token || '');
|
|
})
|
|
.catch((err) => {
|
|
setQldtErr(err.message || 'Lỗi khi tải token QLĐT');
|
|
})
|
|
.finally(() => {
|
|
setLoadingQldt(false);
|
|
});
|
|
}, [isQldtAdmin]);
|
|
|
|
const saveQldtToken = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setQldtMsg('');
|
|
setQldtErr('');
|
|
try {
|
|
await apiQldt.saveToken(qldtToken);
|
|
setQldtMsg('Đã lưu cấu hình QLĐT token');
|
|
} catch (err: any) {
|
|
setQldtErr(err.message || 'Lỗi khi lưu token QLĐT');
|
|
}
|
|
};
|
|
|
|
const submitPw = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setPwErr('');
|
|
setPwMsg('');
|
|
try {
|
|
await changePassword(oldPw, newPw);
|
|
setPwMsg('Đã đổi mật khẩu');
|
|
setOldPw('');
|
|
setNewPw('');
|
|
} catch (e: any) {
|
|
setPwErr(e?.message || 'Lỗi');
|
|
}
|
|
};
|
|
|
|
const displayName = staff?.fullName?.trim() || staff?.email?.split('@')[0] || 'Tài khoản';
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<header className="page-header">
|
|
<h1 className="page-title">Tài khoản của tôi</h1>
|
|
<p className="page-desc">Thông tin đăng nhập, GitHub và bảo mật cá nhân.</p>
|
|
</header>
|
|
|
|
<div className="card" style={{ padding: '1.25rem' }}>
|
|
<h2 className="section-title">Thông tin</h2>
|
|
<p style={{ margin: '0 0 0.25rem', fontWeight: 600 }}>{displayName}</p>
|
|
<p style={{ margin: 0, color: 'var(--text-muted)' }}>{staff?.email}</p>
|
|
</div>
|
|
|
|
<div className="card account-github-card" style={{ padding: '1.25rem' }}>
|
|
<h2 className="section-title">GitHub</h2>
|
|
<p className="page-desc" style={{ marginTop: 0 }}>
|
|
Kết nối tài khoản GitHub của bạn để đẩy bài nộp phòng thi lên repo riêng. Mỗi giáo viên dùng GitHub của mình — không dùng chung token server.
|
|
</p>
|
|
<div className="exam-git-oauth-row">
|
|
{gh.connected ? (
|
|
<>
|
|
<span className="exam-git-connected">
|
|
Đã kết nối: <strong>@{gh.githubLogin}</strong>
|
|
</span>
|
|
<button type="button" className="btn btn-secondary btn-sm" onClick={gh.disconnect}>
|
|
Ngắt kết nối
|
|
</button>
|
|
{!gh.canDeleteRepos && (
|
|
<button type="button" className="btn btn-primary btn-sm" disabled={gh.connecting} onClick={gh.connect}>
|
|
{gh.connecting ? 'Đang mở GitHub...' : 'Cấp quyền xóa repo'}
|
|
</button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary btn-sm"
|
|
disabled={gh.connecting}
|
|
onClick={gh.connect}
|
|
>
|
|
{gh.connecting ? 'Đang mở GitHub...' : 'Kết nối GitHub'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{gh.connected && !gh.canDeleteRepos && (
|
|
<div className="login-error" style={{ marginTop: '0.75rem' }}>
|
|
Token hiện tại chưa có quyền <strong>delete_repo</strong> — không xóa được repo. Bấm <strong>Cấp quyền xóa repo</strong> (hoặc ngắt kết nối rồi kết nối lại).
|
|
</div>
|
|
)}
|
|
{gh.error && <div className="login-error">{gh.error}</div>}
|
|
{gh.message && <div className="login-success">{gh.message}</div>}
|
|
{!gh.connected && (
|
|
<p className="exam-git-hint">
|
|
Sau khi kết nối, vào <strong>Phòng thi</strong> → tab <strong>Bài nộp</strong> → đẩy Git (gộp hoặc từng sinh viên).
|
|
</p>
|
|
)}
|
|
{gh.connected && (
|
|
<>
|
|
<h3 className="section-subtitle" style={{ marginTop: '1rem' }}>Repo đã tạo</h3>
|
|
<GitHubReposPanel connected={gh.connected} canDeleteRepos={gh.canDeleteRepos} />
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{isQldtAdmin && (
|
|
<div className="card" style={{ padding: '1.25rem' }}>
|
|
<h2 className="section-title">Cấu hình QLĐT Token</h2>
|
|
<p className="page-desc" style={{ marginTop: 0 }}>
|
|
Cấu hình token đồng bộ dữ liệu lớp học, môn học và điểm danh từ Cổng đào tạo (QLĐT).
|
|
</p>
|
|
{loadingQldt ? (
|
|
<p style={{ color: 'var(--text-muted)' }}>Đang tải cấu hình...</p>
|
|
) : (
|
|
<form onSubmit={saveQldtToken} className="login-form">
|
|
<label className="login-field">
|
|
<span>Token QLĐT</span>
|
|
<textarea
|
|
style={{
|
|
width: '100%',
|
|
minHeight: '100px',
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.875rem',
|
|
padding: '0.5rem',
|
|
borderRadius: '8px',
|
|
border: '1px solid var(--border-color, #ccc)',
|
|
backgroundColor: 'var(--bg-input, #fff)',
|
|
color: 'var(--text, #333)',
|
|
resize: 'vertical'
|
|
}}
|
|
value={qldtToken}
|
|
onChange={(e) => setQldtToken(e.target.value)}
|
|
placeholder="Nhập JWT Token từ QLĐT..."
|
|
required
|
|
/>
|
|
</label>
|
|
{qldtErr && <div className="login-error">{qldtErr}</div>}
|
|
{qldtMsg && <div className="login-success">{qldtMsg}</div>}
|
|
<button type="submit" className="btn btn-primary" style={{ marginTop: '0.5rem' }}>Lưu cấu hình</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="card" style={{ padding: '1.25rem' }}>
|
|
<h2 className="section-title">Đổi mật khẩu</h2>
|
|
<form onSubmit={submitPw} className="login-form" style={{ maxWidth: 400 }}>
|
|
<label className="login-field">
|
|
<span>Mật khẩu hiện tại</span>
|
|
<input type="password" value={oldPw} onChange={(e) => setOldPw(e.target.value)} required />
|
|
</label>
|
|
<label className="login-field">
|
|
<span>Mật khẩu mới</span>
|
|
<input type="password" value={newPw} onChange={(e) => setNewPw(e.target.value)} required minLength={8} />
|
|
</label>
|
|
{pwErr && <div className="login-error">{pwErr}</div>}
|
|
{pwMsg && <div className="login-success">{pwMsg}</div>}
|
|
<button type="submit" className="btn btn-primary">Lưu</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|