This commit is contained in:
@@ -965,3 +965,20 @@ export const apiExam = {
|
||||
}>;
|
||||
},
|
||||
};
|
||||
|
||||
export const apiQldt = {
|
||||
getToken: async (): Promise<{ token: string }> => {
|
||||
const res = await staffFetch('/qldt-token');
|
||||
if (!res.ok) await parseError(res, 'Không tải được token QLĐT');
|
||||
return res.json();
|
||||
},
|
||||
saveToken: async (token: string): Promise<{ ok: boolean; message: string }> => {
|
||||
const res = await staffFetch('/qldt-token', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
if (!res.ok) await parseError(res, 'Lưu token thất bại');
|
||||
return res.json();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
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();
|
||||
@@ -85,6 +86,40 @@ export function MyAccountTab() {
|
||||
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('');
|
||||
@@ -165,6 +200,45 @@ export function MyAccountTab() {
|
||||
)}
|
||||
</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 }}>
|
||||
|
||||
@@ -65,6 +65,7 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.ExamSubmission{},
|
||||
&models.StaffGitHubAuth{},
|
||||
&models.StaffGitHubRepo{},
|
||||
&models.SystemSetting{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ func atoiDefault(s string, def int) int {
|
||||
}
|
||||
|
||||
// Handler bắt đầu đồng bộ lớp học
|
||||
func StartClassesSyncHandler(db *gorm.DB, client *qldt.Client, job *syncjobs.ClassesSyncJob, token string) fiber.Handler {
|
||||
func StartClassesSyncHandler(db *gorm.DB, client *qldt.Client, job *syncjobs.ClassesSyncJob) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := GetQldtToken(db)
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token QLDT_TOKEN"})
|
||||
}
|
||||
@@ -48,8 +49,9 @@ func GetClassesSyncStatusHandler(job *syncjobs.ClassesSyncJob) fiber.Handler {
|
||||
}
|
||||
|
||||
// Handler bắt đầu đồng bộ toàn bộ sinh viên
|
||||
func StartStudentsSyncHandler(db *gorm.DB, client *qldt.Client, job *syncjobs.StudentsSyncJob, token string) fiber.Handler {
|
||||
func StartStudentsSyncHandler(db *gorm.DB, client *qldt.Client, job *syncjobs.StudentsSyncJob) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := GetQldtToken(db)
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token QLDT_TOKEN"})
|
||||
}
|
||||
|
||||
@@ -27,13 +27,14 @@ var standardShiftTemplate = []struct {
|
||||
}
|
||||
|
||||
// GET /api/classes/:rkId/courses
|
||||
func GetClassCoursesHandler(db *gorm.DB, qldtClient *qldt.Client, token string) fiber.Handler {
|
||||
func GetClassCoursesHandler(db *gorm.DB, qldtClient *qldt.Client) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
||||
}
|
||||
|
||||
token := GetQldtToken(db)
|
||||
var liveErr error
|
||||
if token != "" {
|
||||
res, err := qldtClient.GetClassCourses(context.Background(), token, rkID)
|
||||
@@ -430,7 +431,7 @@ func UpdateAttendanceBulkStatusHandler(db *gorm.DB) fiber.Handler {
|
||||
}
|
||||
|
||||
// POST /api/classes/:rkId/attendance/push-qldt
|
||||
func PushAttendanceToQLDTHandler(db *gorm.DB, qldtClient *qldt.Client, token string) fiber.Handler {
|
||||
func PushAttendanceToQLDTHandler(db *gorm.DB, qldtClient *qldt.Client) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
||||
var req struct {
|
||||
@@ -446,6 +447,7 @@ func PushAttendanceToQLDTHandler(db *gorm.DB, qldtClient *qldt.Client, token str
|
||||
if req.Period < 1 {
|
||||
req.Period = 1
|
||||
}
|
||||
token := GetQldtToken(db)
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "QLDT_TOKEN chưa cấu hình"})
|
||||
}
|
||||
|
||||
62
server/internal/handlers/handlers_config.go
Normal file
62
server/internal/handlers/handlers_config.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"server/internal/models"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetQldtToken lấy token từ database, nếu không có sẽ lấy từ biến môi trường
|
||||
func GetQldtToken(db *gorm.DB) string {
|
||||
var setting models.SystemSetting
|
||||
if err := db.Where("setting_key = ?", "QLDT_TOKEN").First(&setting).Error; err == nil && setting.SettingValue != "" {
|
||||
return setting.SettingValue
|
||||
}
|
||||
return os.Getenv("QLDT_TOKEN")
|
||||
}
|
||||
|
||||
// GetQldtTokenHandler trả về token hiện tại (chỉ hỗ trợ phuocntb@rikkeiacademy.com)
|
||||
func GetQldtTokenHandler(db *gorm.DB) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
email, _ := c.Locals("staffEmail").(string)
|
||||
if email != "phuocntb@rikkeiacademy.com" {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Bạn không có quyền truy cập thông tin này"})
|
||||
}
|
||||
|
||||
token := GetQldtToken(db)
|
||||
return c.JSON(fiber.Map{"token": token})
|
||||
}
|
||||
}
|
||||
|
||||
// SaveQldtTokenHandler lưu token mới vào database (chỉ hỗ trợ phuocntb@rikkeiacademy.com)
|
||||
func SaveQldtTokenHandler(db *gorm.DB) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
email, _ := c.Locals("staffEmail").(string)
|
||||
if email != "phuocntb@rikkeiacademy.com" {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Bạn không có quyền thực hiện thao tác này"})
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Yêu cầu không hợp lệ"})
|
||||
}
|
||||
|
||||
setting := models.SystemSetting{
|
||||
SettingKey: "QLDT_TOKEN",
|
||||
SettingValue: body.Token,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := db.Save(&setting).Error; err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Lỗi lưu cấu hình: " + err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"ok": true, "message": "Đã cập nhật QLDT_TOKEN thành công"})
|
||||
}
|
||||
}
|
||||
@@ -178,3 +178,13 @@ type StudentSession struct {
|
||||
}
|
||||
|
||||
func (StudentSession) TableName() string { return "student_sessions" }
|
||||
|
||||
// SystemSetting lưu các cấu hình hệ thống động
|
||||
type SystemSetting struct {
|
||||
SettingKey string `gorm:"primaryKey;column:setting_key;size:128" json:"settingKey"`
|
||||
SettingValue string `gorm:"column:setting_value;type:text" json:"settingValue"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (SystemSetting) TableName() string { return "system_settings" }
|
||||
|
||||
|
||||
@@ -128,9 +128,9 @@ func main() {
|
||||
staff.Get("/students", handlers.ListAllStudentsHandler(gormDB))
|
||||
|
||||
// Sync endpoints
|
||||
staff.Post("/sync/classes/start", handlers.StartClassesSyncHandler(gormDB, qldtClient, classesJob, qldtToken))
|
||||
staff.Post("/sync/classes/start", handlers.StartClassesSyncHandler(gormDB, qldtClient, classesJob))
|
||||
staff.Get("/sync/classes/status", handlers.GetClassesSyncStatusHandler(classesJob))
|
||||
staff.Post("/sync/students/start", handlers.StartStudentsSyncHandler(gormDB, qldtClient, studentsJob, qldtToken))
|
||||
staff.Post("/sync/students/start", handlers.StartStudentsSyncHandler(gormDB, qldtClient, studentsJob))
|
||||
staff.Get("/sync/students/status", handlers.GetStudentsSyncStatusHandler(studentsJob))
|
||||
|
||||
// Learning Management endpoints
|
||||
@@ -153,13 +153,13 @@ func main() {
|
||||
staff.Get("/classes/:rkId/session-logs", handlers.ListClassSessionLogsHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/online-students", handlers.GetOnlineStudentsHandler(gormDB))
|
||||
|
||||
staff.Get("/classes/:rkId/courses", handlers.GetClassCoursesHandler(gormDB, qldtClient, qldtToken))
|
||||
staff.Get("/classes/:rkId/courses", handlers.GetClassCoursesHandler(gormDB, qldtClient))
|
||||
staff.Post("/classes/:rkId/schedule/apply-template", handlers.ApplyScheduleTemplateHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/attendance", handlers.GetClassAttendanceHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/attendance/shifts", handlers.ListAttendanceShiftsHandler(gormDB))
|
||||
staff.Put("/classes/:rkId/attendance/status", handlers.UpdateAttendanceStatusHandler(gormDB))
|
||||
staff.Put("/classes/:rkId/attendance/bulk-status", handlers.UpdateAttendanceBulkStatusHandler(gormDB))
|
||||
staff.Post("/classes/:rkId/attendance/push-qldt", handlers.PushAttendanceToQLDTHandler(gormDB, qldtClient, qldtToken))
|
||||
staff.Post("/classes/:rkId/attendance/push-qldt", handlers.PushAttendanceToQLDTHandler(gormDB, qldtClient))
|
||||
|
||||
// Chat & quản lý tài khoản
|
||||
staff.Get("/chat/students", handlers.SearchChatStudentsHandler(gormDB))
|
||||
@@ -170,6 +170,8 @@ func main() {
|
||||
staff.Post("/admin/email-domains", handlers.AddEmailDomainHandler(gormDB))
|
||||
staff.Delete("/admin/email-domains/:id", handlers.DeleteEmailDomainHandler(gormDB))
|
||||
staff.Get("/admin/staff", handlers.ListStaffHandler(gormDB))
|
||||
staff.Get("/qldt-token", handlers.GetQldtTokenHandler(gormDB))
|
||||
staff.Post("/qldt-token", handlers.SaveQldtTokenHandler(gormDB))
|
||||
|
||||
// Phòng thi
|
||||
staff.Get("/exam-rooms", handlers.ListExamRoomsHandler(gormDB))
|
||||
|
||||
Reference in New Issue
Block a user