Skip to content
Merged
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
10 changes: 6 additions & 4 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func main() {
}

userRepo := repo.NewUserRepo(database)
divisionRepo := repo.NewDivisionRepo(database)
teamRepo := repo.NewTeamRepo(database)
registrationKeyRepo := repo.NewRegistrationKeyRepo(database)
challengeRepo := repo.NewChallengeRepo(database)
Expand All @@ -98,13 +99,14 @@ func main() {
authSvc := service.NewAuthService(cfg, database, userRepo, registrationKeyRepo, teamRepo, redisClient)
userSvc := service.NewUserService(userRepo, teamRepo)
scoreSvc := service.NewScoreboardService(scoreRepo)
teamSvc := service.NewTeamService(teamRepo)
divisionSvc := service.NewDivisionService(divisionRepo)
teamSvc := service.NewTeamService(teamRepo, divisionRepo)
ctfSvc := service.NewCTFService(cfg, challengeRepo, submissionRepo, redisClient, fileStore)
appConfigSvc := service.NewAppConfigService(appConfigRepo, redisClient, cfg.Cache.AppConfigTTL)
stackClient := stack.NewClient(cfg.Stack.ProvisionerBaseURL, cfg.Stack.ProvisionerAPIKey, cfg.Stack.ProvisionerTimeout)
stackSvc := service.NewStackService(cfg.Stack, stackRepo, challengeRepo, submissionRepo, stackClient, redisClient)

bootstrap.BootstrapAdmin(ctx, cfg, database, userRepo, teamRepo, logger)
bootstrap.BootstrapAdmin(ctx, cfg, database, userRepo, teamRepo, divisionRepo, logger)

if cfg, _, _, err := appConfigSvc.Get(ctx); err != nil {
logger.Warn("app config load warning", slog.Any("error", err))
Expand All @@ -116,10 +118,10 @@ func main() {
defer stop()

sseHub := realtime.NewSSEHub()
leaderboardBus := realtime.NewScoreboardBus(redisClient, cfg, scoreSvc, logger, sseHub)
leaderboardBus := realtime.NewScoreboardBus(redisClient, cfg, scoreSvc, divisionSvc, logger, sseHub)
leaderboardBus.Start(ctx)

router := httpserver.NewRouter(cfg, authSvc, ctfSvc, appConfigSvc, userSvc, scoreSvc, teamSvc, stackSvc, redisClient, logger, sseHub)
router := httpserver.NewRouter(cfg, authSvc, ctfSvc, appConfigSvc, userSvc, scoreSvc, divisionSvc, teamSvc, stackSvc, redisClient, logger, sseHub)
srv := &nethttp.Server{
Addr: cfg.HTTPAddr,
Handler: router,
Expand Down
46 changes: 45 additions & 1 deletion docs/docs/admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,19 @@ Response 200
"created_at": "2026-02-17T12:00:00Z"
}
],
"divisions": [
{
"id": 2,
"name": "고등부",
"created_at": "2026-02-17T09:00:00Z"
}
],
"teams": [
{
"id": 1,
"name": "Alpha",
"division_id": 2,
"division_name": "고등부",
"created_at": "2026-02-17T10:00:00Z",
"member_count": 2,
"total_score": 200
Expand All @@ -117,6 +126,8 @@ Response 200
"role": "user",
"team_id": 1,
"team_name": "Alpha",
"division_id": 2,
"division_name": "고등부",
"blocked_reason": null,
"blocked_at": null,
"created_at": "2026-02-17T10:00:00Z",
Expand Down Expand Up @@ -149,6 +160,7 @@ Notes:
- Password hashes are excluded from user records.
- Challenge flag data is excluded from the report.
- Submission provided flag data are excluded from the report.
- Challenge `points` in the report reflect global dynamic scoring (all divisions combined), not per-division scoring.
- See [report.schema.json](./report.schema.json) for the full schema. (there may be slight differences from the actual response)

Errors:
Expand Down Expand Up @@ -266,7 +278,8 @@ Request

```json
{
"name": "서울고등학교"
"name": "서울고등학교",
"division_id": 2
}
```

Expand All @@ -276,6 +289,37 @@ Response 201
{
"id": 1,
"name": "서울고등학교",
"division_id": 2,
"created_at": "2026-01-26T12:00:00Z"
}
```

Errors:

- 400 `invalid input`
- 401 `invalid token` or `missing authorization` or `invalid authorization`
- 403 `forbidden`

---

## Create Division (Admin)

`POST /api/admin/divisions`

Request

```json
{
"name": "고등부"
}
```

Response 201

```json
{
"id": 2,
"name": "고등부",
"created_at": "2026-01-26T12:00:00Z"
}
```
Expand Down
4 changes: 3 additions & 1 deletion docs/docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ Response 200
"id": 1,
"email": "user@example.com",
"username": "user1",
"role": "user"
"role": "user",
"division_id": 2,
"division_name": "고등부"
}
}
```
Expand Down
8 changes: 7 additions & 1 deletion docs/docs/challenges.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ nav_order: 4

## List Challenges

`GET /api/challenges`
`GET /api/challenges?division_id={id}`

`division_id` is required.

Response 200

Expand Down Expand Up @@ -41,6 +43,10 @@ Notes:
- If a challenge is locked, the response includes only `id`, `title`, `category`, `points`, `initial_points`, `minimum_points`, `solve_count`, `previous_challenge_id`, `previous_challenge_title`, `previous_challenge_category`, `is_active`, and `is_locked`.
- If `ctf_state` is `not_started`, the response only includes `ctf_state`.

Errors:

- 400 `invalid input` (`division_id` required or invalid)

---

## Submit Flag
Expand Down
24 changes: 24 additions & 0 deletions docs/docs/divisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: Divisions
nav_order: 4
---

## List Divisions

`GET /api/divisions`

Response 200

```json
[
{
"id": 2,
"name": "고등부",
"created_at": "2026-01-26T12:00:00Z"
}
]
```

Notes:

- Division identifiers are numeric `id` values (slugs are not supported).
54 changes: 52 additions & 2 deletions docs/docs/report.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@
]
}
},
"divisions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"created_at": {
"type": "string"
}
},
"required": ["id", "name", "created_at"]
}
},
"teams": {
"type": "array",
"items": {
Expand All @@ -99,6 +117,12 @@
"name": {
"type": "string"
},
"division_id": {
"type": "number"
},
"division_name": {
"type": "string"
},
"created_at": {
"type": "string"
},
Expand All @@ -109,7 +133,15 @@
"type": "number"
}
},
"required": ["id", "name", "created_at", "member_count", "total_score"]
"required": [
"id",
"name",
"division_id",
"division_name",
"created_at",
"member_count",
"total_score"
]
}
},
"users": {
Expand All @@ -135,14 +167,31 @@
"team_name": {
"type": "string"
},
"division_id": {
"type": "number"
},
"division_name": {
"type": "string"
},
"created_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
},
"required": ["id", "email", "username", "role", "team_id", "team_name", "created_at", "updated_at"]
"required": [
"id",
"email",
"username",
"role",
"team_id",
"team_name",
"division_id",
"division_name",
"created_at",
"updated_at"
]
}
},
"stacks": {
Expand Down Expand Up @@ -453,6 +502,7 @@
},
"required": [
"challenges",
"divisions",
"teams",
"users",
"stacks",
Expand Down
Loading