@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.
Files changed (143) hide show
  1. package/AGENTS.md +171 -0
  2. package/LICENSE +21 -0
  3. package/Makefile +20 -0
  4. package/backend/.dockerignore +4 -0
  5. package/backend/.golangci.yml +33 -0
  6. package/backend/Dockerfile +19 -0
  7. package/backend/Makefile +39 -0
  8. package/backend/api/.spectral.yaml +31 -0
  9. package/backend/api/oapi-codegen.yaml +8 -0
  10. package/backend/api/openapi.yaml +228 -0
  11. package/backend/cmd/server/main.go +49 -0
  12. package/backend/db/migration/V001__init.sql +25 -0
  13. package/backend/go.mod +84 -0
  14. package/backend/go.sum +305 -0
  15. package/backend/internal/api/api.gen.go +514 -0
  16. package/backend/internal/api/generate.go +4 -0
  17. package/backend/internal/deadline/deadline.go +91 -0
  18. package/backend/internal/deadline/deadline_test.go +82 -0
  19. package/backend/internal/deadline/integration_test.go +71 -0
  20. package/backend/internal/server/api.go +18 -0
  21. package/backend/internal/server/binding.go +52 -0
  22. package/backend/internal/server/binding_test.go +76 -0
  23. package/backend/internal/server/run.go +56 -0
  24. package/backend/internal/server/server.go +79 -0
  25. package/backend/internal/server/server_test.go +235 -0
  26. package/backend/internal/shared/clock/clock.go +34 -0
  27. package/backend/internal/shared/clock/clock_test.go +15 -0
  28. package/backend/internal/shared/config/config.go +66 -0
  29. package/backend/internal/shared/config/config_test.go +46 -0
  30. package/backend/internal/shared/db/fakedriver_test.go +78 -0
  31. package/backend/internal/shared/db/intx.go +113 -0
  32. package/backend/internal/shared/db/intx_test.go +223 -0
  33. package/backend/internal/shared/db/ip.go +65 -0
  34. package/backend/internal/shared/db/ip_test.go +58 -0
  35. package/backend/internal/shared/db/mysqlerr.go +33 -0
  36. package/backend/internal/shared/dbtest/dbtest.go +72 -0
  37. package/backend/internal/shared/dbtest/dbtest_integration_test.go +52 -0
  38. package/backend/internal/shared/text/text.go +105 -0
  39. package/backend/internal/shared/text/text_test.go +20 -0
  40. package/backend/internal/shared/web/accesslog.go +71 -0
  41. package/backend/internal/shared/web/errorcode.go +96 -0
  42. package/backend/internal/shared/web/errors_as.go +11 -0
  43. package/backend/internal/shared/web/json.go +25 -0
  44. package/backend/internal/shared/web/json_test.go +35 -0
  45. package/backend/internal/shared/web/jsonbind.go +179 -0
  46. package/backend/internal/shared/web/problem.go +86 -0
  47. package/backend/internal/shared/web/recover.go +30 -0
  48. package/backend/internal/shared/web/requestid.go +34 -0
  49. package/backend/internal/shared/web/transport.go +91 -0
  50. package/backend/internal/shared/web/web_test.go +183 -0
  51. package/backend/test/apispec/apispec.go +92 -0
  52. package/backend/test/apispec/apispec_test.go +75 -0
  53. package/backend/test/arch/arch_test.go +329 -0
  54. package/backend/test/binding/binding_test.go +132 -0
  55. package/backend/test/contract/schema_test.go +72 -0
  56. package/backend/test/contract/text_cleanup_test.go +56 -0
  57. package/docker-compose.yml +55 -0
  58. package/docs/API/README.md +28 -0
  59. package/docs/API/api-endpoints.md +45 -0
  60. package/docs/API/error-codes.md +76 -0
  61. package/docs/API/response-bodies.md +59 -0
  62. package/docs/Adr/0001-Annex-sqlc-spike.md +160 -0
  63. package/docs/Adr/0001-Backend-in-go.md +92 -0
  64. package/docs/Architecture/Architecture.md +184 -0
  65. package/docs/Architecture/Backend.md +380 -0
  66. package/docs/Architecture/Frontend.md +425 -0
  67. package/docs/Architecture/Sequence.md +286 -0
  68. package/docs/Open-items.md +123 -0
  69. package/docs/PR-plan.md +251 -0
  70. package/docs/PRD.md +498 -0
  71. package/frontend/Dockerfile +18 -0
  72. package/frontend/components.json +20 -0
  73. package/frontend/eslint.config.ts +63 -0
  74. package/frontend/index.html +12 -0
  75. package/frontend/nginx/default.conf +40 -0
  76. package/frontend/nginx/security-headers.conf +5 -0
  77. package/frontend/package-lock.json +9660 -0
  78. package/frontend/package.json +66 -0
  79. package/frontend/public/.gitkeep +0 -0
  80. package/frontend/scripts/check-no-js.mjs +17 -0
  81. package/frontend/scripts/gen-error-codes.mjs +34 -0
  82. package/frontend/src/app/App.test.tsx +163 -0
  83. package/frontend/src/app/App.tsx +14 -0
  84. package/frontend/src/app/access.ts +30 -0
  85. package/frontend/src/app/index.css +194 -0
  86. package/frontend/src/app/layouts.tsx +107 -0
  87. package/frontend/src/app/main.tsx +16 -0
  88. package/frontend/src/app/pages/JoinRequests.tsx +26 -0
  89. package/frontend/src/app/pages/NotFound.tsx +17 -0
  90. package/frontend/src/app/pages/admin.tsx +31 -0
  91. package/frontend/src/app/pages/auth.tsx +126 -0
  92. package/frontend/src/app/pages/ideas.tsx +159 -0
  93. package/frontend/src/app/router.tsx +65 -0
  94. package/frontend/src/app/routes.ts +36 -0
  95. package/frontend/src/app/session.ts +26 -0
  96. package/frontend/src/eslint-plugins.d.ts +1 -0
  97. package/frontend/src/features/.gitkeep +0 -0
  98. package/frontend/src/features/deadline/DeadlineBanner.tsx +34 -0
  99. package/frontend/src/features/deadline/useDeadline.ts +19 -0
  100. package/frontend/src/shared/api/error-codes.generated.ts +45 -0
  101. package/frontend/src/shared/api/index.ts +3 -0
  102. package/frontend/src/shared/api/mocks/problem.ts +9 -0
  103. package/frontend/src/shared/api/mocks/server.ts +4 -0
  104. package/frontend/src/shared/api/problem.test.ts +85 -0
  105. package/frontend/src/shared/api/problem.ts +94 -0
  106. package/frontend/src/shared/api/types.generated.ts +335 -0
  107. package/frontend/src/shared/i18n/i18n.test.ts +51 -0
  108. package/frontend/src/shared/i18n/index.ts +64 -0
  109. package/frontend/src/shared/i18n/locales/en.ts +126 -0
  110. package/frontend/src/shared/i18n/locales/zh-TW.ts +121 -0
  111. package/frontend/src/shared/permissions/.gitkeep +0 -0
  112. package/frontend/src/shared/text/.gitkeep +0 -0
  113. package/frontend/src/shared/time/.gitkeep +0 -0
  114. package/frontend/src/shared/time/offset.ts +11 -0
  115. package/frontend/src/shared/ui/.gitkeep +0 -0
  116. package/frontend/src/shared/ui/button.tsx +63 -0
  117. package/frontend/src/shared/ui/card.tsx +91 -0
  118. package/frontend/src/shared/ui/field.tsx +246 -0
  119. package/frontend/src/shared/ui/form-field.tsx +35 -0
  120. package/frontend/src/shared/ui/input.tsx +20 -0
  121. package/frontend/src/shared/ui/label.tsx +21 -0
  122. package/frontend/src/shared/ui/separator.tsx +27 -0
  123. package/frontend/src/shared/ui/utils.ts +6 -0
  124. package/frontend/src/test-setup.ts +6 -0
  125. package/frontend/src/testdata-alias.test.ts +7 -0
  126. package/frontend/src/vite-env.d.ts +1 -0
  127. package/frontend/tsconfig.json +22 -0
  128. package/frontend/vite.config.ts +18 -0
  129. package/mise.toml +3 -0
  130. package/package.json +26 -4
  131. package/site/.vitepress/config.mts +28 -0
  132. package/site/.vitepress/theme/index.ts +17 -0
  133. package/site/.vitepress/theme/style.css +131 -0
  134. package/site/api-examples.md +49 -0
  135. package/site/index.md +25 -0
  136. package/site/markdown-examples.md +85 -0
  137. package/site/package-lock.json +1998 -0
  138. package/site/package.json +10 -0
  139. package/testdata/error-codes.json +158 -0
  140. package/testdata/schema/error-codes.schema.json +18 -0
  141. package/testdata/schema/text-cleanup.schema.json +30 -0
  142. package/testdata/text-cleanup.json +383 -0
  143. 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,4 @@
1
+ bin
2
+ cover.out*
3
+ **/*_test.go
4
+ test
@@ -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"]
@@ -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,8 @@
1
+ # Standard (non-strict) chi server, Backend.md A1. Output is committed; CI fails on a diff.
2
+ package: api
3
+ output: api.gen.go
4
+ generate:
5
+ models: true
6
+ chi-server: true
7
+ output-options:
8
+ skip-prune: true
@@ -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;