@johnnyboy11234/esg-idea-contest 0.2.0 → 0.2.2

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 (45) hide show
  1. package/backend/api/openapi.yaml +44 -0
  2. package/backend/cmd/server/main.go +20 -2
  3. package/backend/db/migration/V004__session.sql +19 -0
  4. package/backend/go.mod +1 -1
  5. package/backend/internal/account/account_test.go +191 -0
  6. package/backend/internal/account/handler.go +58 -0
  7. package/backend/internal/account/integration_test.go +268 -0
  8. package/backend/internal/account/internal/store/store.go +35 -0
  9. package/backend/internal/account/service.go +153 -0
  10. package/backend/internal/account/validate.go +100 -0
  11. package/backend/internal/api/api.gen.go +82 -0
  12. package/backend/internal/audit/integration_test.go +1 -1
  13. package/backend/internal/auth/csrf.go +212 -0
  14. package/backend/internal/auth/csrf_test.go +281 -0
  15. package/backend/internal/auth/internal/store/session.go +111 -0
  16. package/backend/internal/auth/middleware.go +66 -0
  17. package/backend/internal/auth/middleware_test.go +153 -0
  18. package/backend/internal/auth/session.go +113 -0
  19. package/backend/internal/auth/session_integration_test.go +245 -0
  20. package/backend/internal/auth/session_test.go +36 -0
  21. package/backend/internal/server/api.go +12 -0
  22. package/backend/internal/server/errors.go +10 -25
  23. package/backend/internal/server/errors_test.go +18 -0
  24. package/backend/internal/server/public.go +35 -0
  25. package/backend/internal/server/public_test.go +73 -0
  26. package/backend/internal/server/server.go +7 -3
  27. package/backend/internal/shared/config/config.go +23 -4
  28. package/backend/internal/shared/config/config_test.go +14 -8
  29. package/backend/internal/shared/db/intx_test.go +25 -0
  30. package/backend/internal/shared/db/mysqlerr.go +26 -0
  31. package/backend/internal/shared/security/security.go +6 -2
  32. package/backend/internal/shared/security/security_test.go +6 -4
  33. package/backend/internal/shared/web/problem.go +6 -0
  34. package/backend/internal/shared/web/request.go +27 -0
  35. package/frontend/src/app/App.test.tsx +13 -0
  36. package/frontend/src/app/App.tsx +4 -0
  37. package/frontend/src/app/session.ts +28 -6
  38. package/frontend/src/shared/api/csrf.ts +51 -0
  39. package/frontend/src/shared/api/http.test.tsx +209 -0
  40. package/frontend/src/shared/api/http.ts +91 -0
  41. package/frontend/src/shared/api/index.ts +3 -0
  42. package/frontend/src/shared/text/index.ts +93 -0
  43. package/frontend/src/shared/text/text.test.ts +22 -0
  44. package/package.json +1 -1
  45. package/frontend/src/shared/text/.gitkeep +0 -0
@@ -0,0 +1,93 @@
1
+ // Implements PRD §6.2 clean-up and counting. The back end (Go) implements the
2
+ // same rules; both run testdata/text-cleanup.json.
3
+ // String.prototype.trim is NOT used: its whitespace set differs from Unicode
4
+ // White_Space (it strips U+FEFF and keeps U+0085).
5
+
6
+ export type Problem = '' | 'REQUIRED' | 'INVALID_FORMAT' | 'INVALID_NAME_CHARS' | 'OUT_OF_RANGE';
7
+
8
+ export interface CheckResult {
9
+ cleaned: string;
10
+ problem: Problem;
11
+ }
12
+
13
+ /** Unicode White_Space property: 25 code points. */
14
+ export function isWhitespace(cp: number): boolean {
15
+ return (
16
+ (cp >= 0x0009 && cp <= 0x000d) ||
17
+ cp === 0x0020 ||
18
+ cp === 0x0085 ||
19
+ cp === 0x00a0 ||
20
+ cp === 0x1680 ||
21
+ (cp >= 0x2000 && cp <= 0x200a) ||
22
+ cp === 0x2028 ||
23
+ cp === 0x2029 ||
24
+ cp === 0x202f ||
25
+ cp === 0x205f ||
26
+ cp === 0x3000
27
+ );
28
+ }
29
+
30
+ /** Characters that would end up in the expected filename (PRD §6.2 rule 8). */
31
+ const FORBIDDEN_NAME_CHARS = /[/\\:*?"<>|]/;
32
+ const CONTROL = /\p{Cc}/u;
33
+
34
+ /** Length in Unicode code points (not UTF-16 units). */
35
+ export function countCodePoints(s: string): number {
36
+ return Array.from(s).length;
37
+ }
38
+
39
+ /** Removes leading and trailing White_Space code points. */
40
+ export function trimWhitespace(s: string): string {
41
+ const cps = Array.from(s);
42
+ let start = 0;
43
+ let end = cps.length;
44
+ while (start < end && isWhitespace(cps[start]!.codePointAt(0)!)) start++;
45
+ while (end > start && isWhitespace(cps[end - 1]!.codePointAt(0)!)) end--;
46
+ return cps.slice(start, end).join('');
47
+ }
48
+
49
+ /** NFC, trim, then every whitespace run becomes one ASCII space. */
50
+ export function cleanName(s: string): string {
51
+ const trimmed = trimWhitespace(s.normalize('NFC'));
52
+ let out = '';
53
+ let inRun = false;
54
+ for (const ch of trimmed) {
55
+ if (isWhitespace(ch.codePointAt(0)!)) {
56
+ inRun = true;
57
+ continue;
58
+ }
59
+ if (inRun) {
60
+ out += ' ';
61
+ inRun = false;
62
+ }
63
+ out += ch;
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** NFC, CRLF and CR become LF, then trim. */
69
+ export function cleanSummary(s: string): string {
70
+ return trimWhitespace(s.normalize('NFC').replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
71
+ }
72
+
73
+ function check(cleaned: string, max: number, name: boolean): Problem {
74
+ if (cleaned === '') return 'REQUIRED';
75
+ for (const ch of cleaned) {
76
+ if (CONTROL.test(ch) && (name || ch !== '\n')) return 'INVALID_FORMAT';
77
+ }
78
+ if (name && FORBIDDEN_NAME_CHARS.test(cleaned)) return 'INVALID_NAME_CHARS';
79
+ if (countCodePoints(cleaned) > max) return 'OUT_OF_RANGE';
80
+ return '';
81
+ }
82
+
83
+ /** Cleans a person / Idea name and reports its first problem; `max` is inclusive. */
84
+ export function checkName(s: string, max: number): CheckResult {
85
+ const cleaned = cleanName(s);
86
+ return { cleaned, problem: check(cleaned, max, true) };
87
+ }
88
+
89
+ /** Cleans a summary and reports its first problem; `max` is inclusive. */
90
+ export function checkSummary(s: string, max: number): CheckResult {
91
+ const cleaned = cleanSummary(s);
92
+ return { cleaned, problem: check(cleaned, max, false) };
93
+ }
@@ -0,0 +1,22 @@
1
+ import cleanup from 'testdata/text-cleanup.json';
2
+ import { checkName, checkSummary, countCodePoints, trimWhitespace } from './index';
3
+
4
+ describe('shared/text contract (testdata/text-cleanup.json)', () => {
5
+ it('has cases', () => {
6
+ expect(cleanup.cases.length).toBeGreaterThan(0);
7
+ });
8
+
9
+ it.each(cleanup.cases)('$id', (c) => {
10
+ const r = c.kind === 'name' ? checkName(c.input, c.max) : checkSummary(c.input, c.max);
11
+ expect(r.cleaned).toBe(c.want);
12
+ expect(countCodePoints(r.cleaned)).toBe(c.wantCount);
13
+ expect(r.problem).toBe(c.wantProblem);
14
+ });
15
+ });
16
+
17
+ describe('trimWhitespace', () => {
18
+ it('uses White_Space, not String.prototype.trim', () => {
19
+ expect(trimWhitespace('\u0085a\u0085')).toBe('a');
20
+ expect(trimWhitespace('a')).toBe('a');
21
+ });
22
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnyboy11234/esg-idea-contest",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "ESG Idea Contest registration system: Go backend, React frontend and design documents (source distribution).",
5
5
  "license": "MIT",
6
6
  "author": "Brian Huang",
File without changes