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
4 changes: 3 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func main() {
challengeCommentRepo := repo.NewChallengeCommentRepo(database)
communityRepo := repo.NewCommunityRepo(database)
challengeSeriesRepo := repo.NewChallengeSeriesRepo(database)
popupRepo := repo.NewPopupRepo(database)
scoreRepo := repo.NewScoreboardRepo(database)
stackRepo := repo.NewStackRepo(database)
vmRepo := repo.NewVMRepo(database)
Expand All @@ -99,6 +100,7 @@ func main() {

authSvc := service.NewAuthService(cfg, userRepo, redisClient)
userSvc := service.NewUserService(userRepo, affiliationRepo, profileImageStore)
popupSvc := service.NewPopupService(popupRepo, profileImageStore)
affiliationSvc := service.NewAffiliationService(affiliationRepo)
scoreSvc := service.NewScoreboardService(scoreRepo)
wargameSvc := service.NewWargameService(cfg, challengeRepo, submissionRepo, voteRepo, writeupRepo, challengeCommentRepo, communityRepo, redisClient, fileStore, challengeSeriesRepo)
Expand Down Expand Up @@ -134,7 +136,7 @@ func main() {
leaderboardBus := realtime.NewScoreboardBus(redisClient, cfg, scoreSvc, logger)
leaderboardBus.Start(ctx)

router := httpserver.NewRouter(cfg, authSvc, wargameSvc, userSvc, affiliationSvc, scoreSvc, stackSvc, vmSvc, redisClient, logger)
router := httpserver.NewRouter(cfg, authSvc, wargameSvc, userSvc, affiliationSvc, scoreSvc, stackSvc, vmSvc, popupSvc, redisClient, logger)
srv := &nethttp.Server{
Addr: cfg.HTTPAddr,
Handler: router,
Expand Down
170 changes: 170 additions & 0 deletions docs/docs/admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,176 @@ Validation notes:

---

## List Active Popups

`GET /api/popups/active`

Returns active popups that have an uploaded image. Results are ordered newest first.

Response 200

```json
{
"popups": [
{
"id": 12,
"title": "Event Notice",
"image_key": "popups/8f2c....png",
"image_name": "notice.png",
"link_url": "https://example.com/event",
"is_active": true,
"created_at": "2026-06-06T12:00:00Z",
"updated_at": "2026-06-06T12:00:00Z"
}
]
}
```

Frontend clients build the image URL with `VITE_S3_MEDIA_CDN_BASE_URL + "/" + image_key`. If `link_url` is present, the popup image opens that URL when clicked.

---

## Manage Popups

`GET /api/admin/popups`

Headers

```
Cookie: access_token=<jwt>
```

Response 200

```json
{
"popups": []
}
```

`POST /api/admin/popups`

Request

```json
{
"title": "Event Notice",
"link_url": "https://example.com/event",
"is_active": false
}
```

Response 201

```json
{
"id": 12,
"title": "Event Notice",
"image_key": null,
"image_name": null,
"link_url": "https://example.com/event",
"is_active": false,
"created_by_user_id": 1,
"created_at": "2026-06-06T12:00:00Z",
"updated_at": "2026-06-06T12:00:00Z"
}
```

`PUT /api/admin/popups/{id}`

Request

```json
{
"title": "Updated Notice",
"link_url": "https://example.com/updated",
"is_active": false
}
```

`DELETE /api/admin/popups/{id}`

Response 200

```json
{
"status": "ok"
}
```

Errors:

- 400 `invalid input`
- 401 `invalid token` or `missing access_token cookie`
- 403 `forbidden`
- 404 `popup not found`

Validation notes:

- A popup cannot be created or updated with `is_active: true` until an image has been finalized.
- Deleting a popup image also deactivates that popup.
- `link_url` is optional, but when present it must be an `http://` or `https://` URL.

---

## Popup Image Upload

`POST /api/admin/popups/{id}/image/upload`

Request

```json
{
"filename": "notice.png"
}
```

Response 200

```json
{
"popup": {
"id": 12,
"title": "Event Notice",
"image_key": null,
"image_name": null,
"link_url": "https://example.com/event",
"is_active": false,
"created_at": "2026-06-06T12:00:00Z",
"updated_at": "2026-06-06T12:00:00Z"
},
"upload": {
"url": "https://...",
"method": "POST",
"fields": {
"key": "popups/8f2c....png",
"Content-Type": "image/png"
},
"expires_at": "2026-06-06T12:15:00Z"
}
}
```

After uploading the file to the presigned POST URL, finalize it:

`PUT /api/admin/popups/{id}/image`

```json
{
"key": "popups/8f2c....png",
"filename": "notice.png"
}
```

`DELETE /api/admin/popups/{id}/image`

Validation notes:

- Popup images must use `.png`, `.jpg`, `.jpeg`, or `.webp`.
- Uploads use the S3 Media store and are limited to 10 MB.

---

## Unblock User

`POST /api/admin/users/{id}/unblock`
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useLocale, useT } from './lib/i18n'
import { SITE_CONFIG } from './lib/siteConfig'
import './index.css'
import DismissibleNotice from './components/DismissibleNotice'
import PopupCarousel from './components/PopupCarousel'

interface RouteProps {
routeParams?: Record<string, string>
Expand Down Expand Up @@ -199,6 +200,7 @@ const App = () => {
<p className='mx-auto max-w-7xl px-4 md:px-6'>{t('footer.copyright')}</p>
</footer>
</div>
<PopupCarousel />
</>
)
}
Expand Down
Loading
Loading