@johnnyboy11234/esg-idea-contest 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +171 -0
- package/LICENSE +21 -0
- package/Makefile +20 -0
- package/backend/.dockerignore +4 -0
- package/backend/.golangci.yml +33 -0
- package/backend/Dockerfile +19 -0
- package/backend/Makefile +39 -0
- package/backend/api/.spectral.yaml +31 -0
- package/backend/api/oapi-codegen.yaml +8 -0
- package/backend/api/openapi.yaml +228 -0
- package/backend/cmd/server/main.go +49 -0
- package/backend/db/migration/V001__init.sql +25 -0
- package/backend/go.mod +84 -0
- package/backend/go.sum +305 -0
- package/backend/internal/api/api.gen.go +514 -0
- package/backend/internal/api/generate.go +4 -0
- package/backend/internal/deadline/deadline.go +91 -0
- package/backend/internal/deadline/deadline_test.go +82 -0
- package/backend/internal/deadline/integration_test.go +71 -0
- package/backend/internal/server/api.go +18 -0
- package/backend/internal/server/binding.go +52 -0
- package/backend/internal/server/binding_test.go +76 -0
- package/backend/internal/server/run.go +56 -0
- package/backend/internal/server/server.go +79 -0
- package/backend/internal/server/server_test.go +235 -0
- package/backend/internal/shared/clock/clock.go +34 -0
- package/backend/internal/shared/clock/clock_test.go +15 -0
- package/backend/internal/shared/config/config.go +66 -0
- package/backend/internal/shared/config/config_test.go +46 -0
- package/backend/internal/shared/db/fakedriver_test.go +78 -0
- package/backend/internal/shared/db/intx.go +113 -0
- package/backend/internal/shared/db/intx_test.go +223 -0
- package/backend/internal/shared/db/ip.go +65 -0
- package/backend/internal/shared/db/ip_test.go +58 -0
- package/backend/internal/shared/db/mysqlerr.go +33 -0
- package/backend/internal/shared/dbtest/dbtest.go +72 -0
- package/backend/internal/shared/dbtest/dbtest_integration_test.go +52 -0
- package/backend/internal/shared/text/text.go +105 -0
- package/backend/internal/shared/text/text_test.go +20 -0
- package/backend/internal/shared/web/accesslog.go +71 -0
- package/backend/internal/shared/web/errorcode.go +96 -0
- package/backend/internal/shared/web/errors_as.go +11 -0
- package/backend/internal/shared/web/json.go +25 -0
- package/backend/internal/shared/web/json_test.go +35 -0
- package/backend/internal/shared/web/jsonbind.go +179 -0
- package/backend/internal/shared/web/problem.go +86 -0
- package/backend/internal/shared/web/recover.go +30 -0
- package/backend/internal/shared/web/requestid.go +34 -0
- package/backend/internal/shared/web/transport.go +91 -0
- package/backend/internal/shared/web/web_test.go +183 -0
- package/backend/test/apispec/apispec.go +92 -0
- package/backend/test/apispec/apispec_test.go +75 -0
- package/backend/test/arch/arch_test.go +329 -0
- package/backend/test/binding/binding_test.go +132 -0
- package/backend/test/contract/schema_test.go +72 -0
- package/backend/test/contract/text_cleanup_test.go +56 -0
- package/docker-compose.yml +55 -0
- package/docs/API/README.md +28 -0
- package/docs/API/api-endpoints.md +45 -0
- package/docs/API/error-codes.md +76 -0
- package/docs/API/response-bodies.md +59 -0
- package/docs/Adr/0001-Annex-sqlc-spike.md +160 -0
- package/docs/Adr/0001-Backend-in-go.md +92 -0
- package/docs/Architecture/Architecture.md +184 -0
- package/docs/Architecture/Backend.md +380 -0
- package/docs/Architecture/Frontend.md +425 -0
- package/docs/Architecture/Sequence.md +286 -0
- package/docs/Open-items.md +123 -0
- package/docs/PR-plan.md +251 -0
- package/docs/PRD.md +498 -0
- package/frontend/Dockerfile +18 -0
- package/frontend/components.json +20 -0
- package/frontend/eslint.config.ts +63 -0
- package/frontend/index.html +12 -0
- package/frontend/nginx/default.conf +40 -0
- package/frontend/nginx/security-headers.conf +5 -0
- package/frontend/package-lock.json +9660 -0
- package/frontend/package.json +66 -0
- package/frontend/public/.gitkeep +0 -0
- package/frontend/scripts/check-no-js.mjs +17 -0
- package/frontend/scripts/gen-error-codes.mjs +34 -0
- package/frontend/src/app/App.test.tsx +163 -0
- package/frontend/src/app/App.tsx +14 -0
- package/frontend/src/app/access.ts +30 -0
- package/frontend/src/app/index.css +194 -0
- package/frontend/src/app/layouts.tsx +107 -0
- package/frontend/src/app/main.tsx +16 -0
- package/frontend/src/app/pages/JoinRequests.tsx +26 -0
- package/frontend/src/app/pages/NotFound.tsx +17 -0
- package/frontend/src/app/pages/admin.tsx +31 -0
- package/frontend/src/app/pages/auth.tsx +126 -0
- package/frontend/src/app/pages/ideas.tsx +159 -0
- package/frontend/src/app/router.tsx +65 -0
- package/frontend/src/app/routes.ts +36 -0
- package/frontend/src/app/session.ts +26 -0
- package/frontend/src/eslint-plugins.d.ts +1 -0
- package/frontend/src/features/.gitkeep +0 -0
- package/frontend/src/features/deadline/DeadlineBanner.tsx +34 -0
- package/frontend/src/features/deadline/useDeadline.ts +19 -0
- package/frontend/src/shared/api/error-codes.generated.ts +45 -0
- package/frontend/src/shared/api/index.ts +3 -0
- package/frontend/src/shared/api/mocks/problem.ts +9 -0
- package/frontend/src/shared/api/mocks/server.ts +4 -0
- package/frontend/src/shared/api/problem.test.ts +85 -0
- package/frontend/src/shared/api/problem.ts +94 -0
- package/frontend/src/shared/api/types.generated.ts +335 -0
- package/frontend/src/shared/i18n/i18n.test.ts +51 -0
- package/frontend/src/shared/i18n/index.ts +64 -0
- package/frontend/src/shared/i18n/locales/en.ts +126 -0
- package/frontend/src/shared/i18n/locales/zh-TW.ts +121 -0
- package/frontend/src/shared/permissions/.gitkeep +0 -0
- package/frontend/src/shared/text/.gitkeep +0 -0
- package/frontend/src/shared/time/.gitkeep +0 -0
- package/frontend/src/shared/time/offset.ts +11 -0
- package/frontend/src/shared/ui/.gitkeep +0 -0
- package/frontend/src/shared/ui/button.tsx +63 -0
- package/frontend/src/shared/ui/card.tsx +91 -0
- package/frontend/src/shared/ui/field.tsx +246 -0
- package/frontend/src/shared/ui/form-field.tsx +35 -0
- package/frontend/src/shared/ui/input.tsx +20 -0
- package/frontend/src/shared/ui/label.tsx +21 -0
- package/frontend/src/shared/ui/separator.tsx +27 -0
- package/frontend/src/shared/ui/utils.ts +6 -0
- package/frontend/src/test-setup.ts +6 -0
- package/frontend/src/testdata-alias.test.ts +7 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +18 -0
- package/mise.toml +3 -0
- package/package.json +26 -4
- package/site/.vitepress/config.mts +28 -0
- package/site/.vitepress/theme/index.ts +17 -0
- package/site/.vitepress/theme/style.css +131 -0
- package/site/api-examples.md +49 -0
- package/site/index.md +25 -0
- package/site/markdown-examples.md +85 -0
- package/site/package-lock.json +1998 -0
- package/site/package.json +10 -0
- package/testdata/error-codes.json +158 -0
- package/testdata/schema/error-codes.schema.json +18 -0
- package/testdata/schema/text-cleanup.schema.json +30 -0
- package/testdata/text-cleanup.json +383 -0
- package/README.md +0 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
Project: **ESG Idea Contest** registration system. Employees register, form teams of 1–4 and submit one Idea (one slide file per team); admins manage everything; every write is audited. The repo currently holds **documents only** — no `backend/` or `frontend/` code exists yet (first code PR is `B0-01` in `docs/PR-plan.md`).
|
|
4
|
+
|
|
5
|
+
## Where things are (read in this order)
|
|
6
|
+
|
|
7
|
+
| Need | File |
|
|
8
|
+
|---|---|
|
|
9
|
+
| What to build (requirements, permission matrix, NFRs) | `docs/PRD.md` — **wins over every other document on behaviour** |
|
|
10
|
+
| System design, principles, decision log (D1–D61) | `docs/Architecture/Architecture.md` |
|
|
11
|
+
| Backend design (Go): modules, pipeline, transactions, session/CSRF, audit, data model, tests | `docs/Architecture/Backend.md` |
|
|
12
|
+
| Frontend design | `docs/Architecture/Frontend.md` |
|
|
13
|
+
| Sequence diagrams (create / replace / delete / download, storage consistency) | `docs/Architecture/Sequence.md` |
|
|
14
|
+
| API contract (endpoints, error codes, response bodies) | `docs/API/` (start at `README.md`) |
|
|
15
|
+
| Why Go, and the sqlc findings | `docs/Adr/0001-Backend-in-go.md`, `docs/Adr/0001-Annex-sqlc-spike.md` |
|
|
16
|
+
| Unresolved questions (OI-nn) and owner confirmations | `docs/Open-items.md` — the only tracker for them |
|
|
17
|
+
| PR breakdown, slices S0–S7, PR rules and Definition of Done | `docs/PR-plan.md` (plan only; work items live in `bd`) |
|
|
18
|
+
|
|
19
|
+
Not written yet (do not cite as existing): `docs/modules/`, `docs/runbook.md`, `docs/test-report.md`, `docs/limitations.md`, root `README.md`, `backend/api/openapi.yaml`.
|
|
20
|
+
|
|
21
|
+
If documents disagree: PRD > ADR > Backend/Frontend > Architecture decision log; report the conflict, do not silently pick one. Status markers: **[Decided]** / **[Proposed]** / **[Open]** (open items in `docs/Open-items.md`). Do not treat **[Proposed]** as agreed.
|
|
22
|
+
|
|
23
|
+
## Stack (decided)
|
|
24
|
+
|
|
25
|
+
Go 1.26 · chi · spec-first OpenAPI (oapi-codegen, standard non-strict server) · sqlc + `go-sql-driver/mysql` against MariaDB · Flyway migrations · own session/CSRF/throttling middleware · `log/slog` · minio-go (`MaxRetries: 1`) · React + Vite + TypeScript frontend. Java/Spring text in older decision-log rows is superseded.
|
|
26
|
+
|
|
27
|
+
## Build & Test (target commands; they start to work when the code PRs land)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
golangci-lint run
|
|
31
|
+
go test -race ./... # unit
|
|
32
|
+
go test -race -tags integration ./... # needs Docker (testcontainers-go, real MariaDB + migrations)
|
|
33
|
+
go test -coverpkg=./... -coverprofile=cover.out ./... && go tool cover -func=cover.out # gate: statement coverage >= 80 %
|
|
34
|
+
sqlc generate && go generate ./... # must produce no diff
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Conventions that are easy to get wrong
|
|
38
|
+
|
|
39
|
+
- Packages by feature: `internal/<module>/` with implementation in `internal/<module>/internal/`; dependency direction is checked by an import-graph test and `depguard`. No ORM.
|
|
40
|
+
- Uniform 404: a resource the caller may not see answers the **same uniform 404** as a missing one (and is audited as DENIED). Do not add distinguishing responses.
|
|
41
|
+
- Database time is the only clock. A write succeeds only together with its audit row (track A inside the tx; track B `RecordRejected` in its own tx).
|
|
42
|
+
- Check order: 413 → 401 → CSRF 403 → 404 / role / state, all **before** the multipart body is read. JSON bodies: pointer fields, unknown fields ignored, duplicate keys and wrong types → 400.
|
|
43
|
+
- Unknown transaction outcome (`COMMIT` error not in SQLState class 23/40): keep storage objects; cleanup goes through `pending_file_deletion`.
|
|
44
|
+
- Every PR that adds an error code updates the catalogue, `testdata/error-codes.json`, `docs/API/error-codes.md` and the zh-TW/en dictionaries; every touched permission-matrix cell gets a test.
|
|
45
|
+
- Repo documents are English. No secrets, Vault paths or internal hostnames in `site/`.
|
|
46
|
+
|
|
47
|
+
This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context.
|
|
48
|
+
|
|
49
|
+
> **Architecture in one line:** Issues live in a local Dolt database
|
|
50
|
+
> (`.beads/dolt/`); cross-machine sync uses `bd dolt push/pull` (a
|
|
51
|
+
> git-compatible protocol), stored under `refs/dolt/data` on your git
|
|
52
|
+
> remote — separate from `refs/heads/*` where your code lives.
|
|
53
|
+
> `.beads/issues.jsonl` is a passive export, not the wire protocol.
|
|
54
|
+
>
|
|
55
|
+
> See [sync-concepts](https://github.com/gastownhall/beads/blob/main/docs/core-concepts/sync-concepts.md)
|
|
56
|
+
> for the one-screen overview and anti-patterns (don't treat JSONL as the
|
|
57
|
+
> source of truth; don't `bd import` during normal operation; don't
|
|
58
|
+
> reach for third-party Dolt hosting before trying the default).
|
|
59
|
+
|
|
60
|
+
## Quick Reference
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bd ready # Find available work
|
|
64
|
+
bd show <id> # View issue details
|
|
65
|
+
bd update <id> --claim # Claim work atomically
|
|
66
|
+
bd close <id> # Complete work
|
|
67
|
+
bd dolt push # Push beads data to remote
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Non-Interactive Shell Commands
|
|
71
|
+
|
|
72
|
+
**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.
|
|
73
|
+
|
|
74
|
+
Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input.
|
|
75
|
+
|
|
76
|
+
**Use these forms instead:**
|
|
77
|
+
```bash
|
|
78
|
+
# Force overwrite without prompting
|
|
79
|
+
cp -f source dest # NOT: cp source dest
|
|
80
|
+
mv -f source dest # NOT: mv source dest
|
|
81
|
+
rm -f file # NOT: rm file
|
|
82
|
+
|
|
83
|
+
# For recursive operations
|
|
84
|
+
rm -rf directory # NOT: rm -r directory
|
|
85
|
+
cp -rf source dest # NOT: cp -r source dest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Other commands that may prompt:**
|
|
89
|
+
- `scp` - use `-o BatchMode=yes` for non-interactive
|
|
90
|
+
- `ssh` - use `-o BatchMode=yes` to fail instead of prompting
|
|
91
|
+
- `apt-get` - use `-y` flag
|
|
92
|
+
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var
|
|
93
|
+
|
|
94
|
+
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:1105d646 -->
|
|
95
|
+
## Beads Issue Tracker
|
|
96
|
+
|
|
97
|
+
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
|
|
98
|
+
|
|
99
|
+
### Quick Reference
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bd ready # Find available work
|
|
103
|
+
bd show <id> # View issue details
|
|
104
|
+
bd update <id> --claim # Claim work
|
|
105
|
+
bd close <id> # Complete work
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Rules
|
|
109
|
+
|
|
110
|
+
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
|
|
111
|
+
- Run `bd prime` for detailed command reference and session close protocol
|
|
112
|
+
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
|
|
113
|
+
|
|
114
|
+
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/core-concepts/sync-concepts.md for details and anti-patterns.
|
|
115
|
+
|
|
116
|
+
## Agent Context Profiles
|
|
117
|
+
|
|
118
|
+
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
|
|
119
|
+
|
|
120
|
+
- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
|
|
121
|
+
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
|
|
122
|
+
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
|
|
123
|
+
|
|
124
|
+
## Session Completion
|
|
125
|
+
|
|
126
|
+
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
|
|
127
|
+
|
|
128
|
+
1. **File issues for remaining work** - Create beads for anything that needs follow-up
|
|
129
|
+
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
|
130
|
+
3. **Update issue status** - Close finished work, update in-progress items
|
|
131
|
+
4. **Handle git/sync by active profile**:
|
|
132
|
+
```bash
|
|
133
|
+
# Conservative/minimal/default: report status and proposed commands; wait for approval.
|
|
134
|
+
git status
|
|
135
|
+
|
|
136
|
+
# Team-maintainer opt-in only, unless current instructions forbid it:
|
|
137
|
+
git pull --rebase
|
|
138
|
+
git push
|
|
139
|
+
git status
|
|
140
|
+
```
|
|
141
|
+
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step
|
|
142
|
+
|
|
143
|
+
**Critical rules:**
|
|
144
|
+
- Explicit user or orchestrator instructions override this Beads block.
|
|
145
|
+
- Do not commit or push without clear authority from the active profile or the current user request.
|
|
146
|
+
- If a required sync or push is blocked, stop and report the exact command and error.
|
|
147
|
+
<!-- END BEADS INTEGRATION -->
|
|
148
|
+
|
|
149
|
+
<!-- BEGIN BEADS CODEX SETUP: generated by bd setup codex -->
|
|
150
|
+
## Beads Issue Tracker
|
|
151
|
+
|
|
152
|
+
Use Beads (`bd`) for durable task tracking in repositories that include it. Use the `beads` skill at `.agents/skills/beads/SKILL.md` (project install) or `~/.agents/skills/beads/SKILL.md` (global install) for Beads workflow guidance, then use the `bd` CLI for issue operations.
|
|
153
|
+
|
|
154
|
+
### Quick Reference
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
bd ready # Find available work
|
|
158
|
+
bd show <id> # View issue details
|
|
159
|
+
bd update <id> --claim # Claim work
|
|
160
|
+
bd close <id> # Complete work
|
|
161
|
+
bd prime # Refresh Beads context
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Rules
|
|
165
|
+
|
|
166
|
+
- Use `bd` for all task tracking; do not create markdown TODO lists.
|
|
167
|
+
- Run `bd prime` when Beads context is missing or stale. Codex 0.129.0+ can load Beads context automatically through native hooks; use `/hooks` to inspect or toggle them.
|
|
168
|
+
- Keep persistent project memory in Beads via `bd remember`; do not create ad hoc memory files.
|
|
169
|
+
|
|
170
|
+
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/core-concepts/sync-concepts.md for details and anti-patterns.
|
|
171
|
+
<!-- END BEADS CODEX SETUP -->
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johnson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Makefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Repo-level developer entry points. Backend targets live in backend/Makefile.
|
|
2
|
+
COMPOSE ?= docker compose
|
|
3
|
+
|
|
4
|
+
.PHONY: dev-up dev-down dev-reset migrate
|
|
5
|
+
|
|
6
|
+
## Start MariaDB and MinIO in the background (waits for MariaDB to be healthy).
|
|
7
|
+
dev-up:
|
|
8
|
+
$(COMPOSE) up -d --wait mariadb minio
|
|
9
|
+
|
|
10
|
+
## Stop the stack, keep data volumes.
|
|
11
|
+
dev-down:
|
|
12
|
+
$(COMPOSE) down
|
|
13
|
+
|
|
14
|
+
## Stop the stack and delete all local data.
|
|
15
|
+
dev-reset:
|
|
16
|
+
$(COMPOSE) down -v
|
|
17
|
+
|
|
18
|
+
## Apply Flyway migrations from backend/db/migration (idempotent).
|
|
19
|
+
migrate:
|
|
20
|
+
$(COMPOSE) run --rm flyway
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
version: "2"
|
|
2
|
+
run:
|
|
3
|
+
build-tags: [integration]
|
|
4
|
+
linters:
|
|
5
|
+
default: none
|
|
6
|
+
enable:
|
|
7
|
+
- errcheck
|
|
8
|
+
- govet
|
|
9
|
+
- staticcheck
|
|
10
|
+
- ineffassign
|
|
11
|
+
- unused
|
|
12
|
+
- gosec
|
|
13
|
+
- errorlint
|
|
14
|
+
- depguard
|
|
15
|
+
settings:
|
|
16
|
+
depguard:
|
|
17
|
+
rules:
|
|
18
|
+
no-orm:
|
|
19
|
+
list-mode: lax
|
|
20
|
+
files: ["$all"]
|
|
21
|
+
deny:
|
|
22
|
+
- pkg: gorm.io
|
|
23
|
+
desc: ORMs are banned (Backend.md §2.3); use sqlc
|
|
24
|
+
- pkg: entgo.io
|
|
25
|
+
desc: ORMs are banned (Backend.md §2.3); use sqlc
|
|
26
|
+
- pkg: github.com/uptrace/bun
|
|
27
|
+
desc: ORMs are banned (Backend.md §2.3); use sqlc
|
|
28
|
+
exclusions:
|
|
29
|
+
rules:
|
|
30
|
+
- path: _test\.go
|
|
31
|
+
linters: [gosec, errcheck]
|
|
32
|
+
formatters:
|
|
33
|
+
enable: [gofmt]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Backend image (B0-04 / O0-06). Build context is backend/:
|
|
2
|
+
# docker build -t esg-backend backend
|
|
3
|
+
FROM golang:1.26 AS build
|
|
4
|
+
WORKDIR /src
|
|
5
|
+
COPY go.mod go.sum ./
|
|
6
|
+
RUN go mod download
|
|
7
|
+
COPY . .
|
|
8
|
+
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
|
|
9
|
+
|
|
10
|
+
# Static binary on distroless: no shell, uid 65532, works with a read-only root
|
|
11
|
+
# filesystem (upload spooling needs a /tmp emptyDir, mounted by the chart).
|
|
12
|
+
FROM gcr.io/distroless/static-debian12:nonroot
|
|
13
|
+
COPY --from=build /out/server /server
|
|
14
|
+
# About 75 % of the container memory limit; the chart overrides it together
|
|
15
|
+
# with the limit (Backend.md §12).
|
|
16
|
+
ENV GOMEMLIMIT=384MiB
|
|
17
|
+
EXPOSE 8080 8081
|
|
18
|
+
USER 65532:65532
|
|
19
|
+
ENTRYPOINT ["/server"]
|
package/backend/Makefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Backend developer entry points. `make ci` is what the GitHub Actions workflow runs (.github/workflows/ci.yml).
|
|
2
|
+
GO ?= go
|
|
3
|
+
COVER_MIN ?= 80
|
|
4
|
+
|
|
5
|
+
.PHONY: build lint test test-integration cover ci run generate generate-check spec-lint
|
|
6
|
+
|
|
7
|
+
build:
|
|
8
|
+
CGO_ENABLED=0 $(GO) build -o bin/server ./cmd/server
|
|
9
|
+
|
|
10
|
+
lint:
|
|
11
|
+
golangci-lint run
|
|
12
|
+
|
|
13
|
+
test:
|
|
14
|
+
$(GO) test -race ./...
|
|
15
|
+
|
|
16
|
+
test-integration:
|
|
17
|
+
$(GO) test -race -tags integration ./...
|
|
18
|
+
|
|
19
|
+
# One run: unit + integration (needs Docker), -race, a single merged profile.
|
|
20
|
+
cover:
|
|
21
|
+
$(GO) test -race -tags integration -coverpkg=./... -coverprofile=cover.out ./...
|
|
22
|
+
@# ADR-0001 O2 (recommended, awaiting confirmation): generated code is outside the coverage denominator.
|
|
23
|
+
@grep -v '\.gen\.go:' cover.out > cover.out.tmp && mv -f cover.out.tmp cover.out
|
|
24
|
+
@$(GO) tool cover -func=cover.out | awk -v min=$(COVER_MIN) '/^total:/ { sub("%","",$$3); print "total coverage " $$3 "%"; if ($$3+0 < min) { print "below " min "%"; exit 1 } }'
|
|
25
|
+
|
|
26
|
+
generate:
|
|
27
|
+
$(GO) generate ./...
|
|
28
|
+
|
|
29
|
+
# Generated code is committed; fail when it is stale (B0-12 acceptance: `go generate` no diff).
|
|
30
|
+
generate-check: generate
|
|
31
|
+
@git diff --exit-code -- internal/api || { echo "generated code is stale: run make generate"; exit 1; }
|
|
32
|
+
|
|
33
|
+
spec-lint:
|
|
34
|
+
cd api && npx --yes @stoplight/spectral-cli@6.15.0 lint openapi.yaml --ruleset .spectral.yaml --fail-severity=warn
|
|
35
|
+
|
|
36
|
+
ci: lint generate-check spec-lint cover
|
|
37
|
+
|
|
38
|
+
run:
|
|
39
|
+
$(GO) run ./cmd/server
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
extends: ["spectral:oas"]
|
|
2
|
+
rules:
|
|
3
|
+
info-contact: off
|
|
4
|
+
operation-description: off
|
|
5
|
+
# Backend.md §11: every write declares X-CSRF-Token (checked by the CSRF middleware).
|
|
6
|
+
write-declares-csrf-header:
|
|
7
|
+
description: POST/PUT/DELETE operations must reference the X-CSRF-Token parameter.
|
|
8
|
+
severity: error
|
|
9
|
+
given: "$.paths[*][?(@property === 'post' || @property === 'put' || @property === 'delete')]"
|
|
10
|
+
then:
|
|
11
|
+
field: parameters
|
|
12
|
+
function: schema
|
|
13
|
+
functionOptions:
|
|
14
|
+
schema:
|
|
15
|
+
type: array
|
|
16
|
+
contains:
|
|
17
|
+
type: object
|
|
18
|
+
required: [name, in]
|
|
19
|
+
properties:
|
|
20
|
+
name: { const: X-CSRF-Token }
|
|
21
|
+
in: { const: header }
|
|
22
|
+
# Errors are RFC 9457 problem+json, never another media type.
|
|
23
|
+
errors-are-problem-json:
|
|
24
|
+
description: 4xx/5xx responses must use application/problem+json.
|
|
25
|
+
severity: error
|
|
26
|
+
given: "$.paths[*][*].responses[?(@property.match(/^[45]/))]"
|
|
27
|
+
then:
|
|
28
|
+
field: "$ref"
|
|
29
|
+
function: enumeration
|
|
30
|
+
functionOptions:
|
|
31
|
+
values: ["#/components/responses/Problem"]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
openapi: 3.0.3
|
|
2
|
+
info:
|
|
3
|
+
title: ESG Idea Contest API
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: >
|
|
6
|
+
Single source of the API contract (Backend.md G4). Skeleton: shared components plus the
|
|
7
|
+
operations needed to lock binding rules; the remaining endpoints of docs/API/api-endpoints.md
|
|
8
|
+
are added by their own PRs. Error codes: docs/API/error-codes.md, testdata/error-codes.json.
|
|
9
|
+
servers:
|
|
10
|
+
- url: /api/v1
|
|
11
|
+
security:
|
|
12
|
+
- sessionCookie: []
|
|
13
|
+
tags:
|
|
14
|
+
- name: auth
|
|
15
|
+
- name: deadline
|
|
16
|
+
- name: ideas
|
|
17
|
+
paths:
|
|
18
|
+
/auth/csrf:
|
|
19
|
+
get:
|
|
20
|
+
operationId: getCsrfToken
|
|
21
|
+
tags: [auth]
|
|
22
|
+
summary: Session-bound CSRF token (anonymous before login)
|
|
23
|
+
security: []
|
|
24
|
+
responses:
|
|
25
|
+
'200':
|
|
26
|
+
description: Token to send as X-CSRF-Token on every write
|
|
27
|
+
content:
|
|
28
|
+
application/json:
|
|
29
|
+
schema: { $ref: '#/components/schemas/CsrfTokenResponse' }
|
|
30
|
+
'429': { $ref: '#/components/responses/Problem' }
|
|
31
|
+
'500': { $ref: '#/components/responses/Problem' }
|
|
32
|
+
/auth/logout:
|
|
33
|
+
post:
|
|
34
|
+
operationId: logout
|
|
35
|
+
tags: [auth]
|
|
36
|
+
summary: End the session
|
|
37
|
+
security: []
|
|
38
|
+
parameters:
|
|
39
|
+
- $ref: '#/components/parameters/CsrfToken'
|
|
40
|
+
responses:
|
|
41
|
+
'204': { description: Logged out }
|
|
42
|
+
'403': { $ref: '#/components/responses/Problem' }
|
|
43
|
+
'500': { $ref: '#/components/responses/Problem' }
|
|
44
|
+
/deadline:
|
|
45
|
+
get:
|
|
46
|
+
operationId: getDeadline
|
|
47
|
+
tags: [deadline]
|
|
48
|
+
summary: Submission deadline, state and database server time
|
|
49
|
+
security: []
|
|
50
|
+
responses:
|
|
51
|
+
'200':
|
|
52
|
+
description: Current deadline state
|
|
53
|
+
content:
|
|
54
|
+
application/json:
|
|
55
|
+
schema: { $ref: '#/components/schemas/Deadline' }
|
|
56
|
+
'500': { $ref: '#/components/responses/Problem' }
|
|
57
|
+
'503': { $ref: '#/components/responses/Problem' }
|
|
58
|
+
/ideas:
|
|
59
|
+
get:
|
|
60
|
+
operationId: listIdeas
|
|
61
|
+
tags: [ideas]
|
|
62
|
+
summary: List Ideas visible to the caller
|
|
63
|
+
parameters:
|
|
64
|
+
- name: page
|
|
65
|
+
in: query
|
|
66
|
+
schema: { type: integer, minimum: 1, default: 1 }
|
|
67
|
+
- name: size
|
|
68
|
+
in: query
|
|
69
|
+
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
|
70
|
+
responses:
|
|
71
|
+
'200':
|
|
72
|
+
description: One page of IdeaSummary
|
|
73
|
+
content:
|
|
74
|
+
application/json:
|
|
75
|
+
schema: { $ref: '#/components/schemas/IdeaPage' }
|
|
76
|
+
'401': { $ref: '#/components/responses/Problem' }
|
|
77
|
+
'422': { $ref: '#/components/responses/Problem' }
|
|
78
|
+
'500': { $ref: '#/components/responses/Problem' }
|
|
79
|
+
/ideas/{id}:
|
|
80
|
+
get:
|
|
81
|
+
operationId: getIdea
|
|
82
|
+
tags: [ideas]
|
|
83
|
+
summary: Idea detail
|
|
84
|
+
parameters:
|
|
85
|
+
- $ref: '#/components/parameters/IdeaId'
|
|
86
|
+
responses:
|
|
87
|
+
'200':
|
|
88
|
+
description: IdeaDetail
|
|
89
|
+
content:
|
|
90
|
+
application/json:
|
|
91
|
+
schema: { $ref: '#/components/schemas/IdeaDetail' }
|
|
92
|
+
'401': { $ref: '#/components/responses/Problem' }
|
|
93
|
+
'404': { $ref: '#/components/responses/Problem' }
|
|
94
|
+
'500': { $ref: '#/components/responses/Problem' }
|
|
95
|
+
components:
|
|
96
|
+
securitySchemes:
|
|
97
|
+
sessionCookie:
|
|
98
|
+
type: apiKey
|
|
99
|
+
in: cookie
|
|
100
|
+
name: __Host-session
|
|
101
|
+
parameters:
|
|
102
|
+
CsrfToken:
|
|
103
|
+
name: X-CSRF-Token
|
|
104
|
+
in: header
|
|
105
|
+
required: false
|
|
106
|
+
description: >
|
|
107
|
+
Session-bound token from GET /auth/csrf. Mandatory on every write, but enforced by the CSRF
|
|
108
|
+
middleware (403 CSRF_INVALID), so the generated wrapper must not reject its absence first (400).
|
|
109
|
+
schema: { type: string }
|
|
110
|
+
IdeaId:
|
|
111
|
+
name: id
|
|
112
|
+
in: path
|
|
113
|
+
required: true
|
|
114
|
+
description: A malformed ID answers the uniform 404, not 400.
|
|
115
|
+
schema: { type: string, format: uuid }
|
|
116
|
+
responses:
|
|
117
|
+
Problem:
|
|
118
|
+
description: RFC 9457 problem; `code` is the stable contract (docs/API/error-codes.md)
|
|
119
|
+
content:
|
|
120
|
+
application/problem+json:
|
|
121
|
+
schema: { $ref: '#/components/schemas/Problem' }
|
|
122
|
+
schemas:
|
|
123
|
+
CsrfTokenResponse:
|
|
124
|
+
type: object
|
|
125
|
+
required: [token]
|
|
126
|
+
properties:
|
|
127
|
+
token: { type: string }
|
|
128
|
+
Problem:
|
|
129
|
+
type: object
|
|
130
|
+
required: [type, title, status, code]
|
|
131
|
+
properties:
|
|
132
|
+
type: { type: string, example: about:blank }
|
|
133
|
+
title: { type: string }
|
|
134
|
+
status: { type: integer }
|
|
135
|
+
code: { type: string, description: One of testdata/error-codes.json }
|
|
136
|
+
detail: { type: string }
|
|
137
|
+
requestId: { type: string, format: uuid }
|
|
138
|
+
errors:
|
|
139
|
+
type: array
|
|
140
|
+
items: { $ref: '#/components/schemas/FieldError' }
|
|
141
|
+
FieldError:
|
|
142
|
+
type: object
|
|
143
|
+
required: [field, code]
|
|
144
|
+
properties:
|
|
145
|
+
field: { type: string }
|
|
146
|
+
code: { type: string }
|
|
147
|
+
expected: { type: string }
|
|
148
|
+
Deadline:
|
|
149
|
+
type: object
|
|
150
|
+
required: [deadline, state, serverTime]
|
|
151
|
+
properties:
|
|
152
|
+
deadline: { type: string, format: date-time, nullable: true }
|
|
153
|
+
state: { type: string, enum: [NOT_OPEN, OPEN, CLOSED] }
|
|
154
|
+
serverTime: { type: string, format: date-time, description: Millisecond precision }
|
|
155
|
+
PersonRef:
|
|
156
|
+
type: object
|
|
157
|
+
required: [empNo, name]
|
|
158
|
+
properties:
|
|
159
|
+
empNo: { type: string }
|
|
160
|
+
name: { type: string }
|
|
161
|
+
IdeaSummary:
|
|
162
|
+
type: object
|
|
163
|
+
required: [id, name, category, org, direction, leader, memberCount, myRelation, createdAt, slidesUpdatedAt]
|
|
164
|
+
properties:
|
|
165
|
+
id: { type: string, format: uuid }
|
|
166
|
+
name: { type: string }
|
|
167
|
+
category: { type: string }
|
|
168
|
+
org: { type: string }
|
|
169
|
+
direction: { type: string }
|
|
170
|
+
leader: { $ref: '#/components/schemas/PersonRef' }
|
|
171
|
+
memberCount: { type: integer }
|
|
172
|
+
myRelation: { type: string, enum: [LEADER, MEMBER], nullable: true, description: null for Admin }
|
|
173
|
+
createdAt: { type: string, format: date-time }
|
|
174
|
+
slidesUpdatedAt: { type: string, format: date-time }
|
|
175
|
+
IdeaPage:
|
|
176
|
+
type: object
|
|
177
|
+
required: [items, page, size, total]
|
|
178
|
+
properties:
|
|
179
|
+
items:
|
|
180
|
+
type: array
|
|
181
|
+
items: { $ref: '#/components/schemas/IdeaSummary' }
|
|
182
|
+
page: { type: integer }
|
|
183
|
+
size: { type: integer }
|
|
184
|
+
total: { type: integer }
|
|
185
|
+
IdeaDetail:
|
|
186
|
+
type: object
|
|
187
|
+
required: [id, category, org, direction, name, summary, leader, members, pendingRequests, slides, expectedFilenameBase, createdAt, updatedAt, version]
|
|
188
|
+
properties:
|
|
189
|
+
id: { type: string, format: uuid }
|
|
190
|
+
category: { type: string }
|
|
191
|
+
org: { type: string }
|
|
192
|
+
direction: { type: string }
|
|
193
|
+
name: { type: string }
|
|
194
|
+
summary: { type: string }
|
|
195
|
+
leader: { $ref: '#/components/schemas/PersonRef' }
|
|
196
|
+
members:
|
|
197
|
+
type: array
|
|
198
|
+
items:
|
|
199
|
+
allOf:
|
|
200
|
+
- $ref: '#/components/schemas/PersonRef'
|
|
201
|
+
- type: object
|
|
202
|
+
required: [joinedAt]
|
|
203
|
+
properties:
|
|
204
|
+
joinedAt: { type: string, format: date-time }
|
|
205
|
+
pendingRequests:
|
|
206
|
+
type: array
|
|
207
|
+
nullable: true
|
|
208
|
+
description: null unless Leader or Admin
|
|
209
|
+
items:
|
|
210
|
+
allOf:
|
|
211
|
+
- $ref: '#/components/schemas/PersonRef'
|
|
212
|
+
- type: object
|
|
213
|
+
required: [requestedAt]
|
|
214
|
+
properties:
|
|
215
|
+
requestedAt: { type: string, format: date-time }
|
|
216
|
+
slides:
|
|
217
|
+
type: object
|
|
218
|
+
required: [ext, size, sha256, updatedAt, downloadFilename]
|
|
219
|
+
properties:
|
|
220
|
+
ext: { type: string }
|
|
221
|
+
size: { type: integer, format: int64 }
|
|
222
|
+
sha256: { type: string }
|
|
223
|
+
updatedAt: { type: string, format: date-time }
|
|
224
|
+
downloadFilename: { type: string }
|
|
225
|
+
expectedFilenameBase: { type: string, description: Without extension }
|
|
226
|
+
createdAt: { type: string, format: date-time }
|
|
227
|
+
updatedAt: { type: string, format: date-time }
|
|
228
|
+
version: { type: integer, format: int64 }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Command server is the single deployable of the ESG Idea Contest backend.
|
|
2
|
+
// Dependencies are wired by hand here (no DI framework).
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"database/sql"
|
|
8
|
+
"fmt"
|
|
9
|
+
"os"
|
|
10
|
+
"os/signal"
|
|
11
|
+
"sync/atomic"
|
|
12
|
+
"syscall"
|
|
13
|
+
|
|
14
|
+
_ "github.com/go-sql-driver/mysql"
|
|
15
|
+
|
|
16
|
+
"github.com/brian-huanggg/AwardRegistration/backend/internal/deadline"
|
|
17
|
+
"github.com/brian-huanggg/AwardRegistration/backend/internal/server"
|
|
18
|
+
"github.com/brian-huanggg/AwardRegistration/backend/internal/shared/config"
|
|
19
|
+
"github.com/brian-huanggg/AwardRegistration/backend/internal/shared/web"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
func main() {
|
|
23
|
+
if err := run(); err != nil {
|
|
24
|
+
fmt.Fprintln(os.Stderr, "fatal:", err)
|
|
25
|
+
os.Exit(1)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func run() error {
|
|
30
|
+
cfg, err := config.OS()
|
|
31
|
+
if err != nil {
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
log := web.NewLogger(os.Stdout, cfg.LogLevel)
|
|
35
|
+
|
|
36
|
+
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
37
|
+
defer stop()
|
|
38
|
+
|
|
39
|
+
db, err := sql.Open("mysql", cfg.DBDSN)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return fmt.Errorf("ESG_DB_DSN: %w", err)
|
|
42
|
+
}
|
|
43
|
+
defer func() { _ = db.Close() }()
|
|
44
|
+
|
|
45
|
+
si := server.API{Deadline: deadline.Handler{Store: deadline.SQLStore{DB: db}}}
|
|
46
|
+
|
|
47
|
+
var ready atomic.Bool
|
|
48
|
+
return server.Run(ctx, cfg, log, server.NewPublic(log, server.Mount(si)), &ready, server.DBCheck(db))
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- V001 (B0-01): first schema slice.
|
|
2
|
+
-- contest_setting single-row table holding the submission deadline (PRD 5, 6.9)
|
|
3
|
+
-- audit_log skeleton only; B1-03 adds the remaining columns (expand-only)
|
|
4
|
+
-- Times are database time in UTC; DATETIME has no zone, the DSN pins loc=UTC.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE contest_setting (
|
|
7
|
+
id TINYINT UNSIGNED NOT NULL,
|
|
8
|
+
-- NULL = no deadline ever set (state NOT_OPEN). Second precision (PRD 6.9).
|
|
9
|
+
deadline DATETIME(0) NULL,
|
|
10
|
+
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
11
|
+
PRIMARY KEY (id),
|
|
12
|
+
CONSTRAINT ck_contest_setting_single_row CHECK (id = 1)
|
|
13
|
+
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
|
|
14
|
+
|
|
15
|
+
-- The one and only row; application code only ever UPDATEs it.
|
|
16
|
+
INSERT INTO contest_setting (id, deadline) VALUES (1, NULL);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE audit_log (
|
|
19
|
+
-- Sequence for the optional hash chain (PRD 9 #5); Admin-visible only.
|
|
20
|
+
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
21
|
+
-- Database time is the only clock (PRD 5).
|
|
22
|
+
occurred_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
23
|
+
PRIMARY KEY (id),
|
|
24
|
+
KEY ix_audit_log_occurred_at_id (occurred_at, id)
|
|
25
|
+
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
|