Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions controller/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ func SetupRoutes() {
botGroup.Get("/status", middleware.AuthNeeded(), service.GetWSStatus)
botGroup.Delete("/ws/kick/:session_id", middleware.AuthNeeded(), service.KickConnection)

departmentGroup := (*router).Group("/department")
departmentGroup.Get("/", service.GetDepartmentList)
departmentGroup.Post("/create", middleware.AuthNeeded(), service.CreateDepartment)
departmentGroup.Patch("/", middleware.AuthNeeded(), service.UpdateDepartment)
departmentGroup.Patch("/order", middleware.AuthNeeded(), service.UpdateDepartmentOrder)
departmentGroup.Delete("/:id", middleware.AuthNeeded(), service.DeleteDepartment)
departmentGroup.Post("/:id/member", middleware.AuthNeeded(), service.AddDepartmentMember)
departmentGroup.Delete("/:id/member/:username", middleware.AuthNeeded(), service.RemoveDepartmentMember)
departmentGroup.Patch("/:id/member/:username/leader", middleware.AuthNeeded(), service.UpdateDepartmentMemberLeaderStatus)
departmentGroup.Patch("/:id/member/order", middleware.AuthNeeded(), service.UpdateDepartmentMemberOrder)

wikiGroup := (*router).Group("/wiki")
wikiGroup.Get("/glossary", service.GetGlossaryList)
wikiGroup.Get("/glossary/:id", service.GetGlossaryById)
Expand Down
146 changes: 146 additions & 0 deletions dao/department.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package dao

import (
"fmt"
"necore/database"
"necore/model"

"gorm.io/gorm"
)

func GetDepartmentList() ([]model.Department, error) {
db := database.GetDepartmentDatabase()
var departments []model.Department
err := db.Order("sort_order asc, id asc").Find(&departments).Error
return departments, err
}

func GetDepartmentByID(id string) (*model.Department, error) {
db := database.GetDepartmentDatabase()
var department model.Department
if err := db.Where("id = ?", id).First(&department).Error; err != nil {
return nil, err
}
return &department, nil
}

func CreateDepartment(department model.Department) error {
return database.GetDepartmentDatabase().Create(&department).Error
}

func UpdateDepartment(department model.Department) error {
result := database.GetDepartmentDatabase().
Model(&model.Department{}).
Where("id = ?", department.Id).
Updates(map[string]any{
"name": department.Name,
"description": department.Description,
"icon": department.Icon,
"sort_order": department.SortOrder,
})

if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("department not found")
}
return nil
}

func UpdateDepartmentOrders(orders []model.Department) error {
db := database.GetDepartmentDatabase()
return db.Transaction(func(tx *gorm.DB) error {
for _, department := range orders {
result := tx.Model(&model.Department{}).
Where("id = ?", department.Id).
Update("sort_order", department.SortOrder)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("department not found")
}
}
return nil
})
}

func DeleteDepartment(id string) error {
db := database.GetDepartmentDatabase()
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("department_id = ?", id).Delete(&model.DepartmentMember{}).Error; err != nil {
return err
}
result := tx.Unscoped().Where("id = ?", id).Delete(&model.Department{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("department not found")
}
return nil
})
}

func GetDepartmentMembers(departmentID string) ([]model.DepartmentMember, error) {
db := database.GetDepartmentDatabase()
var members []model.DepartmentMember
err := db.Where("department_id = ?", departmentID).
Order("sort_order asc, username asc").
Find(&members).Error
return members, err
}

func AddDepartmentMember(member model.DepartmentMember) error {
return database.GetDepartmentDatabase().Create(&member).Error
}

func RemoveDepartmentMember(departmentID, username string) error {
result := database.GetDepartmentDatabase().
Unscoped().
Where("department_id = ? AND username = ?", departmentID, username).
Delete(&model.DepartmentMember{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("department member not found")
}
return nil
}

func UpdateDepartmentMemberOrders(departmentID string, members []model.DepartmentMember) error {
db := database.GetDepartmentDatabase()
return db.Transaction(func(tx *gorm.DB) error {
for _, member := range members {
result := tx.Model(&model.DepartmentMember{}).
Where("department_id = ? AND username = ?", departmentID, member.Username).
Updates(map[string]any{
"sort_order": member.SortOrder,
"is_leader": member.IsLeader,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("department member not found")
}
}
return nil
})
}

func UpdateDepartmentMemberLeader(departmentID, username string, isLeader bool) error {
result := database.GetDepartmentDatabase().
Model(&model.DepartmentMember{}).
Where("department_id = ? AND username = ?", departmentID, username).
Update("is_leader", isLeader)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("department member not found")
}
return nil
}
12 changes: 12 additions & 0 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var documentDatabase *gorm.DB

var botTokenDatabase *gorm.DB

var departmentDatabase *gorm.DB

var wikiDatabase *gorm.DB

func ConnectSqlite() {
Expand Down Expand Up @@ -58,6 +60,12 @@ func ConnectSqlite() {
panic("failed to connect wiki database")
}
wikiDatabase.AutoMigrate(&model.Glossary{}, &model.Item{})

departmentDatabase, err = gorm.Open(sqlite.Open("data/department.sqlite3"), &gorm.Config{})
if err != nil {
panic("failed to connect department database")
}
departmentDatabase.AutoMigrate(&model.Department{}, &model.DepartmentMember{})
}

func GetUserDatabase() *gorm.DB {
Expand All @@ -80,6 +88,10 @@ func GetBotTokenDatabase() *gorm.DB {
return botTokenDatabase
}

func GetDepartmentDatabase() *gorm.DB {
return departmentDatabase
}

func GetWikiDatabase() *gorm.DB {
return wikiDatabase
}
13 changes: 13 additions & 0 deletions model/department.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package model

import "gorm.io/gorm"

type Department struct {
gorm.Model

Id string `gorm:"uniqueIndex;not null" json:"id"`
Name string `gorm:"not null" json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
SortOrder int `gorm:"not null;default:0" json:"sortOrder"`
}
12 changes: 12 additions & 0 deletions model/department_member.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package model

import "gorm.io/gorm"

type DepartmentMember struct {
gorm.Model

DepartmentId string `gorm:"index;not null;uniqueIndex:idx_dept_user" json:"departmentId"`
Username string `gorm:"index;not null;uniqueIndex:idx_dept_user" json:"username"`
SortOrder int `gorm:"not null;default:0" json:"sortOrder"`
IsLeader bool `gorm:"not null;default:false" json:"isLeader"`
}
88 changes: 88 additions & 0 deletions routes_and_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func setupTestEnv(t *testing.T) *testEnv {
setGormLoggerSilent(database.GetServerDatabase())
setGormLoggerSilent(database.GetDocumentDatabase())
setGormLoggerSilent(database.GetBotTokenDatabase())
setGormLoggerSilent(database.GetDepartmentDatabase())

// 必须在 Windows 删除 TempDir 前关闭 SQLite 连接池,否则数据库文件会被锁定。
t.Cleanup(func() {
Expand All @@ -76,6 +77,7 @@ func setupTestEnv(t *testing.T) *testEnv {
closeGormDB(t, database.GetServerDatabase())
closeGormDB(t, database.GetDocumentDatabase())
closeGormDB(t, database.GetBotTokenDatabase())
closeGormDB(t, database.GetDepartmentDatabase())
})

must(t, dao.AddUserByUsername("admin", "admin-pass"))
Expand Down Expand Up @@ -281,6 +283,17 @@ func registerRoutes(app *fiber.App) {
botGroup.Delete("/token/:id", middleware.AuthNeeded(), service.DeleteBotToken)
botGroup.Get("/status", middleware.AuthNeeded(), service.GetWSStatus)
botGroup.Delete("/ws/kick/:session_id", middleware.AuthNeeded(), service.KickConnection)

departmentGroup := api.Group("/department")
departmentGroup.Get("/", service.GetDepartmentList)
departmentGroup.Post("/create", middleware.AuthNeeded(), service.CreateDepartment)
departmentGroup.Patch("/", middleware.AuthNeeded(), service.UpdateDepartment)
departmentGroup.Patch("/order", middleware.AuthNeeded(), service.UpdateDepartmentOrder)
departmentGroup.Delete("/:id", middleware.AuthNeeded(), service.DeleteDepartment)
departmentGroup.Post("/:id/member", middleware.AuthNeeded(), service.AddDepartmentMember)
departmentGroup.Delete("/:id/member/:username", middleware.AuthNeeded(), service.RemoveDepartmentMember)
departmentGroup.Patch("/:id/member/:username/leader", middleware.AuthNeeded(), service.UpdateDepartmentMemberLeaderStatus)
departmentGroup.Patch("/:id/member/order", middleware.AuthNeeded(), service.UpdateDepartmentMemberOrder)
}

func must(t *testing.T, err error) {
Expand Down Expand Up @@ -945,6 +958,81 @@ func TestDocument_PathTraversalUpload(t *testing.T) {
}
}

/*
DEPARTMENT ROUTES
*/
func TestDepartmentRoutes(t *testing.T) {
env := setupTestEnv(t)

assertStatus(t, doJSON(t, env, http.MethodGet, "/necore/department/", "", nil), http.StatusOK)

createResp := doJSON(t, env, http.MethodPost, "/necore/department/create", env.adminToken, fiber.Map{
"name": "运维保障部",
"description": "负责服务器与网站稳定运行",
"icon": "/contents/dept/icon.png",
"sortOrder": 1,
})
assertStatus(t, createResp, http.StatusOK)
createBody := decodeBody(t, createResp)
deptID, _ := createBody["id"].(string)
if deptID == "" {
t.Fatalf("create department should return id, got %#v", createBody)
}

assertStatus(t, doJSON(t, env, http.MethodPost, "/necore/department/"+deptID+"/member", env.adminToken, fiber.Map{
"username": "alice",
"sortOrder": 1,
"isLeader": true,
}), http.StatusOK)

listResp := doJSON(t, env, http.MethodGet, "/necore/department/", "", nil)
assertStatus(t, listResp, http.StatusOK)
listBody := decodeBody(t, listResp)
departments, ok := listBody["departments"].([]any)
if !ok || len(departments) != 1 {
t.Fatalf("department list = %#v", listBody["departments"])
}
dept := departments[0].(map[string]any)
members, ok := dept["members"].([]any)
if !ok || len(members) != 1 {
t.Fatalf("members = %#v", dept["members"])
}
member := members[0].(map[string]any)
if member["isLeader"] != true {
t.Fatalf("expected isLeader true after add, got %#v (%T)", member["isLeader"], member["isLeader"])
}

assertStatus(t, doJSON(t, env, http.MethodPatch, "/necore/department/"+deptID+"/member/alice/leader", env.adminToken, fiber.Map{
"isLeader": false,
}), http.StatusOK)

listAfterToggleResp := doJSON(t, env, http.MethodGet, "/necore/department/", "", nil)
assertStatus(t, listAfterToggleResp, http.StatusOK)
listAfterToggleBody := decodeBody(t, listAfterToggleResp)
departmentsAfterToggle, ok := listAfterToggleBody["departments"].([]any)
if !ok || len(departmentsAfterToggle) != 1 {
t.Fatalf("department list after toggle = %#v", listAfterToggleBody["departments"])
}
deptAfterToggle := departmentsAfterToggle[0].(map[string]any)
membersAfterToggle, ok := deptAfterToggle["members"].([]any)
if !ok || len(membersAfterToggle) != 1 {
t.Fatalf("members after toggle = %#v", deptAfterToggle["members"])
}
memberAfterToggle := membersAfterToggle[0].(map[string]any)
if memberAfterToggle["isLeader"] != false {
t.Fatalf("expected isLeader false after toggle, got %#v (%T)", memberAfterToggle["isLeader"], memberAfterToggle["isLeader"])
}

assertStatus(t, doJSON(t, env, http.MethodPatch, "/necore/department/order", env.adminToken, fiber.Map{
"orders": []fiber.Map{
{"id": deptID, "sortOrder": 2},
},
}), http.StatusOK)

assertStatus(t, doJSON(t, env, http.MethodDelete, "/necore/department/"+deptID+"/member/alice", env.adminToken, nil), http.StatusOK)
assertStatus(t, doJSON(t, env, http.MethodDelete, "/necore/department/"+deptID, env.adminToken, nil), http.StatusOK)
}

/*
5. WEBSOCKET SECURITY TEST
*/
Expand Down
Loading
Loading