@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
@@ -0,0 +1,37 @@
1
+ package server
2
+
3
+ import "net/http"
4
+
5
+ // PublicRoutes is the explicit list of operations that need no session
6
+ // (Backend.md §3). It must equal the operations marked `security: []` in
7
+ // api/openapi.yaml; a test locks that. Paths include the /api/v1 prefix.
8
+ var PublicRoutes = []struct{ Method, Path string }{
9
+ {http.MethodGet, "/api/v1/auth/csrf"},
10
+ {http.MethodPost, "/api/v1/auth/login"},
11
+ {http.MethodPost, "/api/v1/auth/register"},
12
+ {http.MethodPost, "/api/v1/auth/logout"},
13
+ {http.MethodGet, "/api/v1/deadline"},
14
+ }
15
+
16
+ // IsPublic reports whether r is exactly one of PublicRoutes. A public path with
17
+ // another method is not public: without a session it answers 401, with one 405.
18
+ func IsPublic(r *http.Request) bool {
19
+ for _, p := range PublicRoutes {
20
+ if r.Method == p.Method && r.URL.Path == p.Path {
21
+ return true
22
+ }
23
+ }
24
+ return false
25
+ }
26
+
27
+ // WriteActions maps the writes that exist today to their audit action, so a
28
+ // CSRF denial is recorded (PRD §6.11). Later PRs add their routes here, with
29
+ // the same exact-path rule; a write on an unknown path is not audited.
30
+ var WriteActions = map[string]string{
31
+ http.MethodPost + " /api/v1/auth/login": "AUTH_LOGIN",
32
+ http.MethodPost + " /api/v1/auth/register": "AUTH_REGISTER",
33
+ http.MethodPost + " /api/v1/auth/logout": "AUTH_LOGOUT",
34
+ }
35
+
36
+ // ActionFor returns the audit action of r, or "" when unknown.
37
+ func ActionFor(r *http.Request) string { return WriteActions[r.Method+" "+r.URL.Path] }
@@ -0,0 +1,73 @@
1
+ package server_test
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "slices"
8
+ "testing"
9
+
10
+ "github.com/getkin/kin-openapi/openapi3"
11
+
12
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/server"
13
+ )
14
+
15
+ // The public list in code must equal the operations the spec marks
16
+ // `security: []`; otherwise the contract and the middleware disagree.
17
+ func TestPublicRoutesMatchOpenAPI(t *testing.T) {
18
+ doc, err := openapi3.NewLoader().LoadFromFile("../../api/openapi.yaml")
19
+ if err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ var want []string
23
+ for path, item := range doc.Paths.Map() {
24
+ for method, op := range item.Operations() {
25
+ if op.Security != nil && len(*op.Security) == 0 {
26
+ want = append(want, method+" /api/v1"+path)
27
+ }
28
+ }
29
+ }
30
+ var got []string
31
+ for _, p := range server.PublicRoutes {
32
+ got = append(got, p.Method+" "+p.Path)
33
+ }
34
+ slices.Sort(want)
35
+ slices.Sort(got)
36
+ if !slices.Equal(want, got) {
37
+ t.Errorf("PublicRoutes\n got %v\n want %v (spec, security: [])", got, want)
38
+ }
39
+ }
40
+
41
+ func TestIsPublicNeedsExactMethodAndPath(t *testing.T) {
42
+ tests := []struct {
43
+ method, path string
44
+ want bool
45
+ }{
46
+ {"GET", "/api/v1/auth/csrf", true},
47
+ {"POST", "/api/v1/auth/logout", true},
48
+ {"POST", "/api/v1/auth/csrf", false},
49
+ {"HEAD", "/api/v1/deadline", false},
50
+ {"GET", "/api/v1/auth/csrf/", false},
51
+ {"GET", "/api/v1/auth/me", false},
52
+ {"GET", "/api/v1/auth/../ideas", false},
53
+ {"GET", "/auth/csrf", false},
54
+ }
55
+ for _, tc := range tests {
56
+ req := httptest.NewRequestWithContext(context.Background(), tc.method, "http://x"+tc.path, nil)
57
+ if got := server.IsPublic(req); got != tc.want {
58
+ t.Errorf("%s %s = %v, want %v", tc.method, tc.path, got, tc.want)
59
+ }
60
+ }
61
+ }
62
+
63
+ func TestActionForCoversPublicWrites(t *testing.T) {
64
+ for _, p := range server.PublicRoutes {
65
+ if p.Method == http.MethodGet {
66
+ continue
67
+ }
68
+ r := httptest.NewRequest(p.Method, p.Path, nil)
69
+ if server.ActionFor(r) == "" {
70
+ t.Errorf("%s %s has no audit action for a CSRF denial", p.Method, p.Path)
71
+ }
72
+ }
73
+ }
@@ -22,8 +22,8 @@ const (
22
22
 
23
23
  // NewPublic builds the public router. Module routes are mounted by `mount`
24
24
  // (nil in the skeleton) under the chain: request ID → access log → recover →
25
- // Accept check.
26
- func NewPublic(log *slog.Logger, mount func(chi.Router)) http.Handler {
25
+ // extra (the session middleware, so 401 beats 406) → Accept check.
26
+ func NewPublic(log *slog.Logger, mount func(chi.Router), extra ...func(http.Handler) http.Handler) http.Handler {
27
27
  r := chi.NewRouter()
28
28
  r.NotFound(web.NotFound)
29
29
  r.MethodNotAllowed(web.MethodNotAllowed)
@@ -32,7 +32,11 @@ func NewPublic(log *slog.Logger, mount func(chi.Router)) http.Handler {
32
32
  }
33
33
  // The chain wraps the router instead of using r.Use: chi skips r.Use
34
34
  // middleware (also for NotFound) while no route is registered.
35
- return web.RequestID(web.AccessLog(log)(web.Recover(log)(web.AcceptCheck(r))))
35
+ h := web.AcceptCheck(r)
36
+ for i := len(extra) - 1; i >= 0; i-- {
37
+ h = extra[i](h)
38
+ }
39
+ return web.RequestID(web.AccessLog(log)(web.Recover(log)(h)))
36
40
  }
37
41
 
38
42
  // Check reports whether one dependency is usable; a non-nil error makes
@@ -18,8 +18,16 @@ type Config struct {
18
18
  LogLevel slog.Level
19
19
  ShutdownTimeout time.Duration // must stay below terminationGracePeriodSeconds
20
20
  ReadyDrainDelay time.Duration // time readiness stays false before Shutdown starts
21
+ CSRFKey string // HMAC key that signs CSRF tokens (>= 32 bytes)
22
+ CSRFKeyPrevious string // optional: still verifies for 8 h after a rotation
21
23
  }
22
24
 
25
+ // DefaultCSRFKey is the throwaway key of the local profile only; every other
26
+ // profile must set ESG_CSRF_KEY.
27
+ const DefaultCSRFKey = "local-dev-csrf-key-not-a-secret-0000"
28
+
29
+ const minCSRFKeyBytes = 32
30
+
23
31
  // DefaultDSN matches docker-compose.yml (throwaway local credentials). The query
24
32
  // pins UTC and matched-row counts (Backend.md §2.3).
25
33
  const DefaultDSN = "esg:esg@tcp(localhost:3306)/esg?parseTime=true&loc=UTC&time_zone=%27%2B00%3A00%27&clientFoundRows=true"
@@ -33,16 +41,27 @@ func Load(getenv func(string) string) (Config, error) {
33
41
  return def
34
42
  }
35
43
  c := Config{
36
- Profile: get("ESG_PROFILE", "local"),
37
- PublicAddr: get("ESG_PUBLIC_ADDR", ":8080"),
38
- ManagementAddr: get("ESG_MANAGEMENT_ADDR", ":8081"),
39
- DBDSN: get("ESG_DB_DSN", DefaultDSN),
44
+ Profile: get("ESG_PROFILE", "local"),
45
+ PublicAddr: get("ESG_PUBLIC_ADDR", ":8080"),
46
+ ManagementAddr: get("ESG_MANAGEMENT_ADDR", ":8081"),
47
+ DBDSN: get("ESG_DB_DSN", DefaultDSN),
48
+ CSRFKey: getenv("ESG_CSRF_KEY"),
49
+ CSRFKeyPrevious: getenv("ESG_CSRF_KEY_PREVIOUS"),
40
50
  }
41
51
  switch c.Profile {
42
52
  case "local", "k3s", "dev":
43
53
  default:
44
54
  return Config{}, fmt.Errorf("ESG_PROFILE %q: want local, k3s or dev", c.Profile)
45
55
  }
56
+ if c.CSRFKey == "" && c.Profile == "local" {
57
+ c.CSRFKey = DefaultCSRFKey
58
+ }
59
+ if len(c.CSRFKey) < minCSRFKeyBytes {
60
+ return Config{}, fmt.Errorf("ESG_CSRF_KEY: need at least %d bytes", minCSRFKeyBytes)
61
+ }
62
+ if c.CSRFKeyPrevious != "" && len(c.CSRFKeyPrevious) < minCSRFKeyBytes {
63
+ return Config{}, fmt.Errorf("ESG_CSRF_KEY_PREVIOUS: need at least %d bytes", minCSRFKeyBytes)
64
+ }
46
65
  if err := c.LogLevel.UnmarshalText([]byte(strings.ToUpper(get("ESG_LOG_LEVEL", "info")))); err != nil {
47
66
  return Config{}, fmt.Errorf("ESG_LOG_LEVEL: %w", err)
48
67
  }
@@ -8,6 +8,8 @@ import (
8
8
  "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/config"
9
9
  )
10
10
 
11
+ const key = "0123456789abcdef0123456789abcdef"
12
+
11
13
  func env(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
12
14
 
13
15
  func TestDefaults(t *testing.T) {
@@ -16,14 +18,14 @@ func TestDefaults(t *testing.T) {
16
18
  t.Fatal(err)
17
19
  }
18
20
  if c.Profile != "local" || c.PublicAddr != ":8080" || c.ManagementAddr != ":8081" ||
19
- c.LogLevel != slog.LevelInfo || c.ShutdownTimeout != 20*time.Second {
21
+ c.LogLevel != slog.LevelInfo || c.ShutdownTimeout != 20*time.Second || c.CSRFKey != config.DefaultCSRFKey {
20
22
  t.Fatalf("%+v", c)
21
23
  }
22
24
  }
23
25
 
24
26
  func TestOverrides(t *testing.T) {
25
27
  c, err := config.Load(env(map[string]string{
26
- "ESG_PROFILE": "dev", "ESG_LOG_LEVEL": "debug", "ESG_SHUTDOWN_TIMEOUT": "5s", "ESG_READY_DRAIN_DELAY": "1s",
28
+ "ESG_PROFILE": "dev", "ESG_CSRF_KEY": key, "ESG_CSRF_KEY_PREVIOUS": key + "2", "ESG_LOG_LEVEL": "debug", "ESG_SHUTDOWN_TIMEOUT": "5s", "ESG_READY_DRAIN_DELAY": "1s",
27
29
  }))
28
30
  if err != nil || c.Profile != "dev" || c.LogLevel != slog.LevelDebug || c.ReadyDrainDelay != time.Second {
29
31
  t.Fatalf("%+v %v", c, err)
@@ -32,12 +34,16 @@ func TestOverrides(t *testing.T) {
32
34
 
33
35
  func TestInvalid(t *testing.T) {
34
36
  for name, m := range map[string]map[string]string{
35
- "profile": {"ESG_PROFILE": "prod"},
36
- "log level": {"ESG_LOG_LEVEL": "loud"},
37
- "timeout": {"ESG_SHUTDOWN_TIMEOUT": "soon"},
38
- "zero timeout": {"ESG_SHUTDOWN_TIMEOUT": "0s"},
39
- "drain": {"ESG_READY_DRAIN_DELAY": "x"},
40
- "same address": {"ESG_PUBLIC_ADDR": ":9", "ESG_MANAGEMENT_ADDR": ":9"},
37
+ "profile": {"ESG_PROFILE": "prod"},
38
+ "log level": {"ESG_LOG_LEVEL": "loud"},
39
+ "timeout": {"ESG_SHUTDOWN_TIMEOUT": "soon"},
40
+ "zero timeout": {"ESG_SHUTDOWN_TIMEOUT": "0s"},
41
+ "drain": {"ESG_READY_DRAIN_DELAY": "x"},
42
+ "short csrf key": {"ESG_CSRF_KEY": "short"},
43
+ "short previous key": {"ESG_CSRF_KEY": key, "ESG_CSRF_KEY_PREVIOUS": "short"},
44
+ "dev needs csrf key": {"ESG_PROFILE": "dev"},
45
+ "k3s needs csrf key": {"ESG_PROFILE": "k3s"},
46
+ "same address": {"ESG_PUBLIC_ADDR": ":9", "ESG_MANAGEMENT_ADDR": ":9"},
41
47
  } {
42
48
  if _, err := config.Load(env(m)); err == nil {
43
49
  t.Errorf("%s: want error", name)
@@ -3,6 +3,8 @@ package web
3
3
  import (
4
4
  "encoding/json"
5
5
  "net/http"
6
+ "strconv"
7
+ "time"
6
8
 
7
9
  "github.com/google/uuid"
8
10
  )
@@ -28,6 +30,9 @@ type Error struct {
28
30
  Detail string
29
31
  Fields []FieldError
30
32
  Cause error
33
+ // RetryAfter, when positive, is sent as Retry-After in whole seconds,
34
+ // rounded up (minimum 1; PRD §6.5 throttling).
35
+ RetryAfter time.Duration
31
36
 
32
37
  // AuditReason is the real reason of a rejected attempt (CSRF, role, state,
33
38
  // not-yours, not-found). It is stored in the row's after_json, never sent
@@ -94,6 +99,10 @@ func WriteProblem(w http.ResponseWriter, r *http.Request, e *Error) {
94
99
  }
95
100
  w.Header().Set("Content-Type", ProblemContentType)
96
101
  w.Header().Set("Cache-Control", "no-store")
102
+ if e.RetryAfter > 0 {
103
+ secs := max(1, int64((e.RetryAfter+time.Second-1)/time.Second))
104
+ w.Header().Set("Retry-After", strconv.FormatInt(secs, 10))
105
+ }
97
106
  w.WriteHeader(e.Status)
98
107
  _ = json.NewEncoder(w).Encode(body)
99
108
  }
@@ -0,0 +1,31 @@
1
+ package web_test
2
+
3
+ import (
4
+ "net/http/httptest"
5
+ "testing"
6
+ "time"
7
+
8
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/web"
9
+ )
10
+
11
+ func TestRetryAfterWholeSecondsRoundedUpMinimumOne(t *testing.T) {
12
+ tests := []struct {
13
+ d time.Duration
14
+ want string
15
+ }{
16
+ {0, ""},
17
+ {time.Millisecond, "1"},
18
+ {time.Second, "1"},
19
+ {1001 * time.Millisecond, "2"},
20
+ {15 * time.Minute, "900"},
21
+ }
22
+ for _, tc := range tests {
23
+ e := web.NewError(web.CodeLoginThrottled, "")
24
+ e.RetryAfter = tc.d
25
+ rec := httptest.NewRecorder()
26
+ web.WriteProblem(rec, httptest.NewRequest("POST", "/", nil), e)
27
+ if got := rec.Header().Get("Retry-After"); got != tc.want {
28
+ t.Errorf("%v: Retry-After = %q, want %q", tc.d, got, tc.want)
29
+ }
30
+ }
31
+ }
@@ -161,3 +161,16 @@ describe('accessibility', () => {
161
161
  expect((await axe(container)).violations).toEqual([]);
162
162
  });
163
163
  });
164
+
165
+ describe('global 401 (Frontend.md §5.2)', () => {
166
+ it('goes to /login?next=<current> with a session-ended notice and forgets the user', async () => {
167
+ const { router } = await open('/ideas?page=2', 'USER');
168
+ await screen.findByRole('heading', { level: 1 });
169
+ server.use(http.get(`${API}/ideas`, () => problem(401, 'UNAUTHENTICATED')));
170
+ const { apiFetch } = await import('@/shared/api');
171
+ await expect(apiFetch('/ideas')).rejects.toMatchObject({ status: 401 });
172
+ await waitFor(() => expect(router.state.location.pathname).toBe('/login'));
173
+ expect(router.state.location.search).toBe(`?next=${encodeURIComponent('/ideas?page=2')}`);
174
+ expect(router.state.location.state).toEqual({ notice: 'sessionEnded' });
175
+ });
176
+ });
@@ -1,11 +1,15 @@
1
+ import { useEffect } from 'react';
1
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
3
  import { RouterProvider, createBrowserRouter, type DataRouter } from 'react-router';
4
+ import { setUnauthenticatedHandler } from '@/shared/api';
3
5
  import { routes } from './router';
6
+ import { handleSessionEnded } from './session';
4
7
 
5
8
  const queryClient = new QueryClient();
6
9
  const browserRouter = createBrowserRouter(routes);
7
10
 
8
11
  export function App({ router = browserRouter, client = queryClient }: { router?: DataRouter; client?: QueryClient }) {
12
+ useEffect(() => setUnauthenticatedHandler(() => handleSessionEnded(client, router)), [client, router]);
9
13
  return (
10
14
  <QueryClientProvider client={client}>
11
15
  <RouterProvider router={router} />
@@ -6,58 +6,58 @@
6
6
  Frontend.md §8.1 (4.5:1 text, 3:1 components). */
7
7
  :root {
8
8
  --radius: 0.5rem;
9
- --background: oklch(0.9751 0.0127 244.2507);
10
- --foreground: oklch(0.3729 0.0306 259.7328);
11
- --card: oklch(1.0000 0 0);
12
- --card-foreground: oklch(0.3729 0.0306 259.7328);
13
- --popover: oklch(1.0000 0 0);
14
- --popover-foreground: oklch(0.3729 0.0306 259.7328);
15
- --primary: oklch(0.7227 0.1920 149.5793);
16
- --primary-foreground: oklch(1.0000 0 0);
17
- --secondary: oklch(0.9514 0.0250 236.8242);
18
- --secondary-foreground: oklch(0.4461 0.0263 256.8018);
19
- --muted: oklch(0.9670 0.0029 264.5419);
20
- --muted-foreground: oklch(0.5510 0.0234 264.3637);
21
- --accent: oklch(0.9505 0.0507 163.0508);
22
- --accent-foreground: oklch(0.3729 0.0306 259.7328);
23
- --destructive: oklch(0.6368 0.2078 25.3313);
24
- --border: oklch(0.9276 0.0058 264.5313);
25
- --input: oklch(0.9276 0.0058 264.5313);
26
- --ring: oklch(0.7227 0.1920 149.5793);
27
- --destructive-foreground: oklch(1.0000 0 0);
28
- --chart-1: oklch(0.7227 0.1920 149.5793);
29
- --chart-2: oklch(0.6959 0.1491 162.4796);
30
- --chart-3: oklch(0.5960 0.1274 163.2254);
31
- --chart-4: oklch(0.5081 0.1049 165.6121);
32
- --chart-5: oklch(0.4318 0.0865 166.9128);
33
- --sidebar: oklch(0.9514 0.0250 236.8242);
34
- --sidebar-foreground: oklch(0.3729 0.0306 259.7328);
35
- --sidebar-primary: oklch(0.7227 0.1920 149.5793);
36
- --sidebar-primary-foreground: oklch(1.0000 0 0);
37
- --sidebar-accent: oklch(0.9505 0.0507 163.0508);
38
- --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328);
39
- --sidebar-border: oklch(0.9276 0.0058 264.5313);
40
- --sidebar-ring: oklch(0.7227 0.1920 149.5793);
41
- --font-sans: DM Sans, sans-serif;
42
- --font-serif: Lora, serif;
43
- --font-mono: IBM Plex Mono, monospace;
44
- --shadow-color: hsl(0 0% 0%);
45
- --shadow-opacity: 0.1;
46
- --shadow-blur: 8px;
47
- --shadow-spread: -1px;
9
+ --background: oklch(0.9911 0 0);
10
+ --foreground: oklch(0.2046 0 0);
11
+ --card: oklch(0.9911 0 0);
12
+ --card-foreground: oklch(0.2046 0 0);
13
+ --popover: oklch(0.9911 0 0);
14
+ --popover-foreground: oklch(0.4386 0 0);
15
+ --primary: oklch(0.8348 0.1302 160.9080);
16
+ --primary-foreground: oklch(0.2626 0.0147 166.4589);
17
+ --secondary: oklch(0.9940 0 0);
18
+ --secondary-foreground: oklch(0.2046 0 0);
19
+ --muted: oklch(0.9461 0 0);
20
+ --muted-foreground: oklch(0.2435 0 0);
21
+ --accent: oklch(0.9461 0 0);
22
+ --accent-foreground: oklch(0.2435 0 0);
23
+ --destructive: oklch(0.5523 0.1927 32.7272);
24
+ --border: oklch(0.9037 0 0);
25
+ --input: oklch(0.9731 0 0);
26
+ --ring: oklch(0.8348 0.1302 160.9080);
27
+ --destructive-foreground: oklch(0.9934 0.0032 17.2118);
28
+ --chart-1: oklch(0.8348 0.1302 160.9080);
29
+ --chart-2: oklch(0.6231 0.1880 259.8145);
30
+ --chart-3: oklch(0.6056 0.2189 292.7172);
31
+ --chart-4: oklch(0.7686 0.1647 70.0804);
32
+ --chart-5: oklch(0.6959 0.1491 162.4796);
33
+ --sidebar: oklch(0.9911 0 0);
34
+ --sidebar-foreground: oklch(0.5452 0 0);
35
+ --sidebar-primary: oklch(0.8348 0.1302 160.9080);
36
+ --sidebar-primary-foreground: oklch(0.2626 0.0147 166.4589);
37
+ --sidebar-accent: oklch(0.9461 0 0);
38
+ --sidebar-accent-foreground: oklch(0.2435 0 0);
39
+ --sidebar-border: oklch(0.9037 0 0);
40
+ --sidebar-ring: oklch(0.8348 0.1302 160.9080);
41
+ --font-sans: Outfit, sans-serif;
42
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
43
+ --font-mono: monospace;
44
+ --shadow-color: #000000;
45
+ --shadow-opacity: 0.17;
46
+ --shadow-blur: 3px;
47
+ --shadow-spread: 0px;
48
48
  --shadow-offset-x: 0px;
49
- --shadow-offset-y: 4px;
50
- --letter-spacing: 0em;
49
+ --shadow-offset-y: 1px;
50
+ --letter-spacing: 0.025em;
51
51
  --spacing: 0.25rem;
52
- --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
53
- --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
54
- --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
55
- --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
56
- --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
57
- --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
58
- --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
59
- --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
60
- --tracking-normal: 0em;
52
+ --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
53
+ --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
54
+ --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
55
+ --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
56
+ --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
57
+ --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
58
+ --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
59
+ --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
60
+ --tracking-normal: 0.025em;
61
61
  }
62
62
 
63
63
  @theme inline {
@@ -83,9 +83,9 @@
83
83
  --color-border: var(--border);
84
84
  --color-input: var(--input);
85
85
  --color-ring: var(--ring);
86
- --font-sans: DM Sans, sans-serif;
87
- --font-mono: IBM Plex Mono, monospace;
88
- --font-serif: Lora, serif;
86
+ --font-sans: Outfit, sans-serif;
87
+ --font-mono: monospace;
88
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
89
89
  --radius: 0.5rem;
90
90
  --tracking-tighter: calc(var(--tracking-normal) - 0.05em);
91
91
  --tracking-tight: calc(var(--tracking-normal) - 0.025em);
@@ -139,56 +139,56 @@
139
139
  }
140
140
 
141
141
  .dark {
142
- --background: oklch(0.2077 0.0398 265.7549);
143
- --foreground: oklch(0.8717 0.0093 258.3382);
144
- --card: oklch(0.2795 0.0368 260.0310);
145
- --card-foreground: oklch(0.8717 0.0093 258.3382);
146
- --popover: oklch(0.2795 0.0368 260.0310);
147
- --popover-foreground: oklch(0.8717 0.0093 258.3382);
148
- --primary: oklch(0.7729 0.1535 163.2231);
149
- --primary-foreground: oklch(0.2077 0.0398 265.7549);
150
- --secondary: oklch(0.3351 0.0331 260.9120);
151
- --secondary-foreground: oklch(0.7118 0.0129 286.0665);
152
- --muted: oklch(0.2463 0.0275 259.9628);
153
- --muted-foreground: oklch(0.5510 0.0234 264.3637);
154
- --accent: oklch(0.3729 0.0306 259.7328);
155
- --accent-foreground: oklch(0.7118 0.0129 286.0665);
156
- --destructive: oklch(0.6368 0.2078 25.3313);
157
- --destructive-foreground: oklch(0.2077 0.0398 265.7549);
158
- --border: oklch(0.4461 0.0263 256.8018);
159
- --input: oklch(0.4461 0.0263 256.8018);
160
- --ring: oklch(0.7729 0.1535 163.2231);
161
- --chart-1: oklch(0.7729 0.1535 163.2231);
162
- --chart-2: oklch(0.7845 0.1325 181.9120);
163
- --chart-3: oklch(0.7227 0.1920 149.5793);
164
- --chart-4: oklch(0.6959 0.1491 162.4796);
165
- --chart-5: oklch(0.5960 0.1274 163.2254);
142
+ --background: oklch(0.1822 0 0);
143
+ --foreground: oklch(0.9288 0.0126 255.5078);
144
+ --card: oklch(0.2046 0 0);
145
+ --card-foreground: oklch(0.9288 0.0126 255.5078);
146
+ --popover: oklch(0.2603 0 0);
147
+ --popover-foreground: oklch(0.7348 0 0);
148
+ --primary: oklch(0.4365 0.1044 156.7556);
149
+ --primary-foreground: oklch(0.9213 0.0135 167.1556);
150
+ --secondary: oklch(0.2603 0 0);
151
+ --secondary-foreground: oklch(0.9851 0 0);
152
+ --muted: oklch(0.2393 0 0);
153
+ --muted-foreground: oklch(0.7122 0 0);
154
+ --accent: oklch(0.3132 0 0);
155
+ --accent-foreground: oklch(0.9851 0 0);
156
+ --destructive: oklch(0.3123 0.0852 29.7877);
157
+ --destructive-foreground: oklch(0.9368 0.0045 34.3092);
158
+ --border: oklch(0.2809 0 0);
159
+ --input: oklch(0.2603 0 0);
160
+ --ring: oklch(0.8003 0.1821 151.7110);
161
+ --chart-1: oklch(0.8003 0.1821 151.7110);
162
+ --chart-2: oklch(0.7137 0.1434 254.6240);
163
+ --chart-3: oklch(0.7090 0.1592 293.5412);
164
+ --chart-4: oklch(0.8369 0.1644 84.4286);
165
+ --chart-5: oklch(0.7845 0.1325 181.9120);
166
166
  --radius: 0.5rem;
167
- --sidebar: oklch(0.2795 0.0368 260.0310);
168
- --sidebar-foreground: oklch(0.8717 0.0093 258.3382);
169
- --sidebar-primary: oklch(0.7729 0.1535 163.2231);
170
- --sidebar-primary-foreground: oklch(0.2077 0.0398 265.7549);
171
- --sidebar-accent: oklch(0.3729 0.0306 259.7328);
172
- --sidebar-accent-foreground: oklch(0.7118 0.0129 286.0665);
173
- --sidebar-border: oklch(0.4461 0.0263 256.8018);
174
- --sidebar-ring: oklch(0.7729 0.1535 163.2231);
175
- --font-sans: DM Sans, sans-serif;
176
- --font-serif: Lora, serif;
177
- --font-mono: IBM Plex Mono, monospace;
178
- --shadow-color: hsl(0 0% 0%);
179
- --shadow-opacity: 0.1;
180
- --shadow-blur: 8px;
181
- --shadow-spread: -1px;
167
+ --sidebar: oklch(0.1822 0 0);
168
+ --sidebar-foreground: oklch(0.6301 0 0);
169
+ --sidebar-primary: oklch(0.4365 0.1044 156.7556);
170
+ --sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
171
+ --sidebar-accent: oklch(0.3132 0 0);
172
+ --sidebar-accent-foreground: oklch(0.9851 0 0);
173
+ --sidebar-border: oklch(0.2809 0 0);
174
+ --sidebar-ring: oklch(0.8003 0.1821 151.7110);
175
+ --font-sans: Outfit, sans-serif;
176
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
177
+ --font-mono: monospace;
178
+ --shadow-color: #000000;
179
+ --shadow-opacity: 0.17;
180
+ --shadow-blur: 3px;
181
+ --shadow-spread: 0px;
182
182
  --shadow-offset-x: 0px;
183
- --shadow-offset-y: 4px;
184
- --letter-spacing: 0em;
183
+ --shadow-offset-y: 1px;
184
+ --letter-spacing: 0.025em;
185
185
  --spacing: 0.25rem;
186
- --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
187
- --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
188
- --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
189
- --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
190
- --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
191
- --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
192
- --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
193
- --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
186
+ --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
187
+ --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
188
+ --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
189
+ --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
190
+ --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
191
+ --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
192
+ --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
193
+ --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
194
194
  }