@johnnyboy11234/esg-idea-contest 0.2.1 → 0.2.3

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 (49) hide show
  1. package/backend/api/openapi.yaml +70 -0
  2. package/backend/cmd/server/main.go +17 -1
  3. package/backend/internal/account/logging_integration_test.go +70 -0
  4. package/backend/internal/api/api.gen.go +125 -0
  5. package/backend/internal/auth/csrf.go +212 -0
  6. package/backend/internal/auth/csrf_test.go +281 -0
  7. package/backend/internal/auth/internal/store/session.go +63 -5
  8. package/backend/internal/auth/logging_integration_test.go +88 -0
  9. package/backend/internal/auth/login.go +252 -0
  10. package/backend/internal/auth/login_integration_test.go +421 -0
  11. package/backend/internal/auth/login_test.go +68 -0
  12. package/backend/internal/auth/logout.go +68 -0
  13. package/backend/internal/auth/logout_integration_test.go +124 -0
  14. package/backend/internal/auth/me.go +40 -0
  15. package/backend/internal/auth/me_integration_test.go +70 -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 +20 -1
  19. package/backend/internal/server/api.go +19 -0
  20. package/backend/internal/server/public.go +37 -0
  21. package/backend/internal/server/public_test.go +73 -0
  22. package/backend/internal/server/server.go +7 -3
  23. package/backend/internal/shared/config/config.go +23 -4
  24. package/backend/internal/shared/config/config_test.go +14 -8
  25. package/backend/internal/shared/web/problem.go +9 -0
  26. package/backend/internal/shared/web/retryafter_test.go +31 -0
  27. package/frontend/src/app/App.test.tsx +13 -0
  28. package/frontend/src/app/App.tsx +4 -0
  29. package/frontend/src/app/index.css +102 -102
  30. package/frontend/src/app/layouts.tsx +75 -18
  31. package/frontend/src/app/pages/auth.tsx +52 -78
  32. package/frontend/src/app/pages/login.test.tsx +218 -0
  33. package/frontend/src/app/pages/register.test.tsx +231 -0
  34. package/frontend/src/app/router.tsx +83 -34
  35. package/frontend/src/app/session.ts +71 -18
  36. package/frontend/src/features/auth/LoginForm.tsx +145 -0
  37. package/frontend/src/features/auth/RegisterForm.tsx +211 -0
  38. package/frontend/src/features/auth/login.ts +23 -0
  39. package/frontend/src/features/auth/register.ts +105 -0
  40. package/frontend/src/shared/api/csrf.ts +51 -0
  41. package/frontend/src/shared/api/http.test.tsx +209 -0
  42. package/frontend/src/shared/api/http.ts +91 -0
  43. package/frontend/src/shared/api/index.ts +3 -0
  44. package/frontend/src/shared/i18n/locales/en.ts +15 -2
  45. package/frontend/src/shared/i18n/locales/zh-TW.ts +15 -2
  46. package/frontend/src/shared/text/index.ts +20 -0
  47. package/frontend/src/shared/text/text.test.ts +28 -1
  48. package/frontend/vite.config.ts +5 -1
  49. package/package.json +1 -1
@@ -1,3 +1,6 @@
1
1
  export { ApiError, parseApiError } from './problem';
2
2
  export type { ErrorCode, FieldError } from './problem';
3
+ export { apiFetch, setUnauthenticatedHandler } from './http';
4
+ export type { ApiRequestInit } from './http';
5
+ export { clearCsrfToken, getCsrfToken } from './csrf';
3
6
  export type { paths, components } from './types.generated';
@@ -21,6 +21,7 @@ export const en = {
21
21
  main: 'Main navigation',
22
22
  skip: 'Skip to content',
23
23
  administrator: 'Administrator',
24
+ signOut: 'Sign out',
24
25
  signIn: 'Sign in',
25
26
  register: 'Register',
26
27
  },
@@ -56,8 +57,20 @@ export const en = {
56
57
  leaderEmpNo: 'Leader employee ID',
57
58
  },
58
59
  pages: {
59
- login: { title: 'Sign in to your account', description: 'Enter your employee ID and password.', noAccount: 'No account yet?', panelTitle: 'ESG Idea Contest', panelBody: 'Register, form a team and submit your Idea.' },
60
- register: { title: 'Create an account', description: 'Enter your details below to register.', passwordHint: 'At least 8 characters; see the rules shown while you type.', haveAccount: 'Already registered?' },
60
+ login: { title: 'Sign in to your account', description: 'Enter your employee ID and password.', noAccount: 'No account yet?', panelTitle: 'ESG Idea Contest', panelBody: 'Register, form a team and submit your Idea.', submitting: 'Signing in…', sessionEnded: 'Your session has ended. Please sign in again.' },
61
+ register: {
62
+ title: 'Create an account',
63
+ description: 'Enter your details below to register.',
64
+ empNoHint: '6 digits.',
65
+ emailHint: 'Letters, digits and symbols only, no spaces.',
66
+ passwordHint: '12–64 printable ASCII characters, with at least 3 of: upper case, lower case, digit, symbol.',
67
+ mismatch: 'The passwords do not match.',
68
+ haveAccount: 'Already registered?',
69
+ submit: 'Create account',
70
+ submitting: 'Creating account…',
71
+ summaryTitle: 'Please fix the following',
72
+ success: 'Your account was created. Sign in to continue.',
73
+ },
61
74
  ideaList: { search: 'Search', apply: 'Apply filters', create: 'Create an Idea', empty: 'No Ideas yet.' },
62
75
  ideaForm: { slidesHint: 'PDF or PPTX, up to 5 MiB.' },
63
76
  ideaCreate: { title: 'Create an Idea', submit: 'Submit Idea' },
@@ -18,6 +18,7 @@ export const zhTW: Dictionary = {
18
18
  main: '主要導覽',
19
19
  skip: '跳到主要內容',
20
20
  administrator: '管理員',
21
+ signOut: '登出',
21
22
  signIn: '登入',
22
23
  register: '註冊',
23
24
  },
@@ -53,8 +54,20 @@ export const zhTW: Dictionary = {
53
54
  leaderEmpNo: '組長員工編號',
54
55
  },
55
56
  pages: {
56
- login: { title: '登入您的帳號', description: '請輸入員工編號與密碼。', noAccount: '還沒有帳號?', panelTitle: 'ESG 創意競賽', panelBody: '註冊、組隊並提交您的創意。' },
57
- register: { title: '建立帳號', description: '請在下方輸入資料以完成註冊。', passwordHint: '至少 8 個字元,輸入時會顯示規則檢查。', haveAccount: '已經註冊?' },
57
+ login: { title: '登入您的帳號', description: '請輸入員工編號與密碼。', noAccount: '還沒有帳號?', panelTitle: 'ESG 創意競賽', panelBody: '註冊、組隊並提交您的創意。', submitting: '登入中…', sessionEnded: '您的登入已結束,請重新登入。' },
58
+ register: {
59
+ title: '建立帳號',
60
+ description: '請在下方輸入資料以完成註冊。',
61
+ empNoHint: '6 位數字。',
62
+ emailHint: '僅限英數字與符號,不可包含空白。',
63
+ passwordHint: '12–64 個可列印 ASCII 字元,且至少包含大寫、小寫、數字、符號其中 3 類。',
64
+ mismatch: '兩次輸入的密碼不一致。',
65
+ haveAccount: '已經註冊?',
66
+ submit: '建立帳號',
67
+ submitting: '建立中…',
68
+ summaryTitle: '請修正以下項目',
69
+ success: '帳號已建立,請登入。',
70
+ },
58
71
  ideaList: { search: '搜尋', apply: '套用篩選', create: '建立提案', empty: '目前沒有提案。' },
59
72
  ideaForm: { slidesHint: 'PDF 或 PPTX,最大 5 MiB。' },
60
73
  ideaCreate: { title: '建立提案', submit: '送出提案' },
@@ -91,3 +91,23 @@ export function checkSummary(s: string, max: number): CheckResult {
91
91
  const cleaned = cleanSummary(s);
92
92
  return { cleaned, problem: check(cleaned, max, false) };
93
93
  }
94
+
95
+ export type EmailProblem = '' | 'REQUIRED' | 'INVALID_FORMAT' | 'OUT_OF_RANGE';
96
+
97
+ const MAX_EMAIL = 254;
98
+
99
+ /**
100
+ * Pre-check of PRD §6.3 (the server is the judge): trim, printable ASCII only
101
+ * (U+0021–U+007E, so no whitespace anywhere), exactly one `@` with non-empty
102
+ * sides, at most 254 characters. `cleaned` is lower-cased as the server stores it.
103
+ * First problem in report order: REQUIRED → INVALID_FORMAT → OUT_OF_RANGE.
104
+ */
105
+ export function checkEmail(s: string): { cleaned: string; problem: EmailProblem } {
106
+ const e = trimWhitespace(s);
107
+ if (e === '') return { cleaned: '', problem: 'REQUIRED' };
108
+ if (!/^[\u0021-\u007e]+$/.test(e)) return { cleaned: e, problem: 'INVALID_FORMAT' };
109
+ const at = e.indexOf('@');
110
+ if (at <= 0 || at === e.length - 1 || e.indexOf('@', at + 1) !== -1) return { cleaned: e, problem: 'INVALID_FORMAT' };
111
+ if (e.length > MAX_EMAIL) return { cleaned: e, problem: 'OUT_OF_RANGE' };
112
+ return { cleaned: e.toLowerCase(), problem: '' };
113
+ }
@@ -1,5 +1,5 @@
1
1
  import cleanup from 'testdata/text-cleanup.json';
2
- import { checkName, checkSummary, countCodePoints, trimWhitespace } from './index';
2
+ import { checkEmail, checkName, checkSummary, countCodePoints, trimWhitespace } from './index';
3
3
 
4
4
  describe('shared/text contract (testdata/text-cleanup.json)', () => {
5
5
  it('has cases', () => {
@@ -20,3 +20,30 @@ describe('trimWhitespace', () => {
20
20
  expect(trimWhitespace('a')).toBe('a');
21
21
  });
22
22
  });
23
+
24
+ describe('checkEmail (PRD §6.3)', () => {
25
+ it.each([
26
+ ['a@x.com', 'a@x.com', ''],
27
+ [' A@X.Com ', 'a@x.com', ''],
28
+ [' a@x ', 'a@x', ''],
29
+ ['a@localhost', 'a@localhost', ''],
30
+ ['', '', 'REQUIRED'],
31
+ [' ', '', 'REQUIRED'],
32
+ ['a b@x.com', 'a b@x.com', 'INVALID_FORMAT'],
33
+ ['a@x .com', 'a@x .com', 'INVALID_FORMAT'],
34
+ ['é@x.com', 'é@x.com', 'INVALID_FORMAT'],
35
+ ['ax.com', 'ax.com', 'INVALID_FORMAT'],
36
+ ['@x.com', '@x.com', 'INVALID_FORMAT'],
37
+ ['a@', 'a@', 'INVALID_FORMAT'],
38
+ ['a@@x.com', 'a@@x.com', 'INVALID_FORMAT'],
39
+ ['a@b@x.com', 'a@b@x.com', 'INVALID_FORMAT'],
40
+ ])('%j', (input, cleaned, problem) => {
41
+ expect(checkEmail(input)).toEqual({ cleaned, problem });
42
+ });
43
+
44
+ it('allows 254 characters and rejects 255', () => {
45
+ const at = (n: number) => `${'a'.repeat(n - 2)}@x`;
46
+ expect(checkEmail(at(254)).problem).toBe('');
47
+ expect(checkEmail(at(255)).problem).toBe('OUT_OF_RANGE');
48
+ });
49
+ });
@@ -9,7 +9,11 @@ const testdata = fileURLToPath(new URL('../testdata', import.meta.url));
9
9
  export default defineConfig({
10
10
  plugins: [react(), tailwindcss()],
11
11
  resolve: { alias: { '@': src, testdata } },
12
- server: { fs: { allow: ['..'] } },
12
+ server: {
13
+ fs: { allow: ['..'] },
14
+ // The app calls same-origin /api/v1; in dev the Go backend (ESG_PUBLIC_ADDR, default :8080) answers it.
15
+ proxy: { '/api': process.env.ESG_API_TARGET ?? 'http://localhost:8080' },
16
+ },
13
17
  test: {
14
18
  environment: 'jsdom',
15
19
  globals: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnyboy11234/esg-idea-contest",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",