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 { useAuth } from '../auth/AuthContext';
|
||||||
import { useGitHubConnection } from '../hooks/useGitHubConnection';
|
import { useGitHubConnection } from '../hooks/useGitHubConnection';
|
||||||
import { GitHubReposPanel } from './GitHubReposPanel';
|
import { GitHubReposPanel } from './GitHubReposPanel';
|
||||||
import { OrganizationSection } from './OrganizationSection';
|
import { OrganizationSection } from './OrganizationSection';
|
||||||
|
import { apiQldt } from '../api';
|
||||||
|
|
||||||
export function ChangePasswordPage({ forced }: { forced?: boolean }) {
|
export function ChangePasswordPage({ forced }: { forced?: boolean }) {
|
||||||
const { changePassword, logout } = useAuth();
|
const { changePassword, logout } = useAuth();
|
||||||
@@ -85,6 +86,40 @@ export function MyAccountTab() {
|
|||||||
const [pwMsg, setPwMsg] = useState('');
|
const [pwMsg, setPwMsg] = useState('');
|
||||||
const [pwErr, setPwErr] = 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) => {
|
const submitPw = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setPwErr('');
|
setPwErr('');
|
||||||
@@ -165,6 +200,45 @@ export function MyAccountTab() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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' }}>
|
<div className="card" style={{ padding: '1.25rem' }}>
|
||||||
<h2 className="section-title">Đổi mật khẩu</h2>
|
<h2 className="section-title">Đổi mật khẩu</h2>
|
||||||
<form onSubmit={submitPw} className="login-form" style={{ maxWidth: 400 }}>
|
<form onSubmit={submitPw} className="login-form" style={{ maxWidth: 400 }}>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.ExamSubmission{},
|
&models.ExamSubmission{},
|
||||||
&models.StaffGitHubAuth{},
|
&models.StaffGitHubAuth{},
|
||||||
&models.StaffGitHubRepo{},
|
&models.StaffGitHubRepo{},
|
||||||
|
&models.SystemSetting{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ func atoiDefault(s string, def int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handler bắt đầu đồng bộ lớp học
|
// 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 {
|
return func(c *fiber.Ctx) error {
|
||||||
|
token := GetQldtToken(db)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token QLDT_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
|
// 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 {
|
return func(c *fiber.Ctx) error {
|
||||||
|
token := GetQldtToken(db)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing token QLDT_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
|
// 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 {
|
return func(c *fiber.Ctx) error {
|
||||||
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token := GetQldtToken(db)
|
||||||
var liveErr error
|
var liveErr error
|
||||||
if token != "" {
|
if token != "" {
|
||||||
res, err := qldtClient.GetClassCourses(context.Background(), token, rkID)
|
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
|
// 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 {
|
return func(c *fiber.Ctx) error {
|
||||||
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -446,6 +447,7 @@ func PushAttendanceToQLDTHandler(db *gorm.DB, qldtClient *qldt.Client, token str
|
|||||||
if req.Period < 1 {
|
if req.Period < 1 {
|
||||||
req.Period = 1
|
req.Period = 1
|
||||||
}
|
}
|
||||||
|
token := GetQldtToken(db)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "QLDT_TOKEN chưa cấu hình"})
|
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" }
|
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))
|
staff.Get("/students", handlers.ListAllStudentsHandler(gormDB))
|
||||||
|
|
||||||
// Sync endpoints
|
// 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.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))
|
staff.Get("/sync/students/status", handlers.GetStudentsSyncStatusHandler(studentsJob))
|
||||||
|
|
||||||
// Learning Management endpoints
|
// Learning Management endpoints
|
||||||
@@ -153,13 +153,13 @@ func main() {
|
|||||||
staff.Get("/classes/:rkId/session-logs", handlers.ListClassSessionLogsHandler(gormDB))
|
staff.Get("/classes/:rkId/session-logs", handlers.ListClassSessionLogsHandler(gormDB))
|
||||||
staff.Get("/classes/:rkId/online-students", handlers.GetOnlineStudentsHandler(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.Post("/classes/:rkId/schedule/apply-template", handlers.ApplyScheduleTemplateHandler(gormDB))
|
||||||
staff.Get("/classes/:rkId/attendance", handlers.GetClassAttendanceHandler(gormDB))
|
staff.Get("/classes/:rkId/attendance", handlers.GetClassAttendanceHandler(gormDB))
|
||||||
staff.Get("/classes/:rkId/attendance/shifts", handlers.ListAttendanceShiftsHandler(gormDB))
|
staff.Get("/classes/:rkId/attendance/shifts", handlers.ListAttendanceShiftsHandler(gormDB))
|
||||||
staff.Put("/classes/:rkId/attendance/status", handlers.UpdateAttendanceStatusHandler(gormDB))
|
staff.Put("/classes/:rkId/attendance/status", handlers.UpdateAttendanceStatusHandler(gormDB))
|
||||||
staff.Put("/classes/:rkId/attendance/bulk-status", handlers.UpdateAttendanceBulkStatusHandler(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
|
// Chat & quản lý tài khoản
|
||||||
staff.Get("/chat/students", handlers.SearchChatStudentsHandler(gormDB))
|
staff.Get("/chat/students", handlers.SearchChatStudentsHandler(gormDB))
|
||||||
@@ -170,6 +170,8 @@ func main() {
|
|||||||
staff.Post("/admin/email-domains", handlers.AddEmailDomainHandler(gormDB))
|
staff.Post("/admin/email-domains", handlers.AddEmailDomainHandler(gormDB))
|
||||||
staff.Delete("/admin/email-domains/:id", handlers.DeleteEmailDomainHandler(gormDB))
|
staff.Delete("/admin/email-domains/:id", handlers.DeleteEmailDomainHandler(gormDB))
|
||||||
staff.Get("/admin/staff", handlers.ListStaffHandler(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
|
// Phòng thi
|
||||||
staff.Get("/exam-rooms", handlers.ListExamRoomsHandler(gormDB))
|
staff.Get("/exam-rooms", handlers.ListExamRoomsHandler(gormDB))
|
||||||
|
|||||||
Reference in New Issue
Block a user