@stacksjs/defaults 0.72.49 → 0.72.54

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 (33) hide show
  1. package/ai/skills/stacks-browse/scripts/browse.ts +63 -4
  2. package/ide/vscode/package.json +1 -1
  3. package/lang/de.yml +0 -1
  4. package/package.json +7 -2
  5. package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +8 -0
  6. package/resources/components/Dashboard/Auth/Login.stx +8 -0
  7. package/resources/components/Dashboard/Auth/LoginDashboard.stx +5 -1
  8. package/resources/components/Dashboard/Auth/MagicLink.stx +94 -0
  9. package/resources/components/Dashboard/Auth/Register.stx +7 -0
  10. package/resources/components/Dashboard/Auth/RegisterDashboard.stx +5 -1
  11. package/resources/components/Dashboard/Auth/ResetPassword.stx +163 -0
  12. package/resources/components/Dashboard/Auth/ResetPasswordDashboard.stx +95 -0
  13. package/resources/layouts/403.stx +15 -148
  14. package/resources/layouts/404.stx +7 -2
  15. package/resources/views/auth/magic/[token].stx +24 -0
  16. package/resources/views/errors/404.stx +42 -0
  17. package/resources/views/forgot-password.stx +13 -0
  18. package/resources/views/login.stx +68 -0
  19. package/resources/views/password/reset/[token].stx +18 -0
  20. package/resources/views/register.stx +64 -0
  21. package/vcs/github/workflows/ci.yml +87 -0
  22. package/views/[...all].stx +0 -7
  23. package/views/about.md +0 -3
  24. package/views/auth/login.stx +0 -1
  25. package/views/auth/magic/[token].stx +0 -88
  26. package/views/auth/signup.stx +0 -120
  27. package/views/forum/category.stx +0 -92
  28. package/views/forum/index.stx +0 -209
  29. package/views/forum/new-topic.stx +0 -113
  30. package/views/forum/profile.stx +0 -1
  31. package/views/forum/topic.stx +0 -143
  32. package/views/hello-world/[name].stx +0 -47
  33. package/views/index.stx +0 -21
@@ -1,4 +1,8 @@
1
1
  <script server>
2
+ // Laravel 5.7's 403 illustration (MIT), the same scene the framework's own
3
+ // error pages use, so an app that opts into this layout matches them. Pass
4
+ // `backgroundImage` to swap it for anything else.
5
+ const backgroundImage = props.backgroundImage || '/svgs/403.svg'
2
6
  const appName = props.appName || 'Your App'
3
7
  const supportEmail = props.supportEmail || 'support@example.com'
4
8
  </script>
@@ -17,30 +21,20 @@ const supportEmail = props.supportEmail || 'support@example.com'
17
21
  <h2 class="mb-4 font-bold text-6xl text-slate-900">
18
22
  Access Denied
19
23
  </h2>
20
- <p class="mb-8 font-bold text-8xl bounce" style="animation-duration: 1s; animation-iteration-count: 2;">
24
+ <p class="mb-8 font-bold text-8xl">
21
25
  403
22
26
  </p>
23
27
  <p class="mb-8 text-slate-600 text-xl">
24
28
  Sorry, you don't have permission to access this page.
25
29
  </p>
26
30
 
27
- <div class="flex gap-4 sm:flex-row">
28
- <a
29
- href="/"
30
- class="flex items-center justify-center px-6 py-3 w-full sm:w-auto text-white bg-slate-500 hover:bg-slate-600 rounded-lg duration-200 transition-colors"
31
- >
32
- <i class="mr-2 h-5 w-5 i-hugeicons-home-03"></i>
33
- Back to Home
34
- </a>
35
-
36
- <a
37
- href="javascript:history.back()"
38
- class="flex items-center justify-center px-6 py-3 w-full sm:w-auto text-slate-700 hover:bg-slate-50 border border-slate-300 rounded-lg duration-200 transition-colors"
39
- >
40
- <i class="mr-2 h-5 w-5 i-hugeicons-arrow-left-02"></i>
41
- Go Back
42
- </a>
43
- </div>
31
+ <a
32
+ href="/"
33
+ class="inline-flex items-center justify-center px-6 py-3 text-white bg-slate-500 hover:bg-slate-600 rounded-lg duration-200 transition-colors"
34
+ >
35
+ <i class="mr-2 h-5 w-5 i-hugeicons-home-03"></i>
36
+ Back to Home
37
+ </a>
44
38
  </div>
45
39
 
46
40
  <div class="mt-16 pt-8 border-slate-200 border-t">
@@ -54,135 +48,8 @@ const supportEmail = props.supportEmail || 'support@example.com'
54
48
  </div>
55
49
  </div>
56
50
 
57
- <!-- Right side SVG -->
58
- <div class="flex overflow-hidden relative items-center justify-center h-[100vh] w-full lg:w-1/2">
59
- <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
60
- <defs>
61
- <linearGradient id="a" x1="50%" x2="50%" y1="100%" y2="0%">
62
- <stop offset="0" stop-color="#76c3c3"/>
63
- <stop offset="1" stop-color="#183468"/>
64
- </linearGradient>
65
- <linearGradient id="b" x1="100%" x2="0%" y1="50%" y2="50%">
66
- <stop offset="0" stop-color="#486587"/>
67
- <stop offset=".3323" stop-color="#183352"/>
68
- <stop offset=".6667" stop-color="#264a6e"/>
69
- <stop offset="1" stop-color="#183352"/>
70
- </linearGradient>
71
- <linearGradient id="c" x1="49.87%" x2="48.5%" y1="3.62%" y2="100%">
72
- <stop offset="0" stop-color="#e0f2fa"/>
73
- <stop offset=".0898" stop-color="#89bed6"/>
74
- <stop offset=".3298" stop-color="#1e3c6e"/>
75
- <stop offset="1" stop-color="#1b376b"/>
76
- </linearGradient>
77
- <linearGradient id="d">
78
- <stop offset="0" stop-color="#b0ddf1"/>
79
- <stop offset="1" stop-color="#325c82"/>
80
- </linearGradient>
81
- <linearGradient id="e" x1="49.87%" x2="49.87%" xlink:href="#d" y1="3.62%" y2="77.75%"/>
82
- <linearGradient id="f">
83
- <stop offset="0" stop-color="#1d3a6d"/>
84
- <stop offset="1" stop-color="#467994"/>
85
- </linearGradient>
86
- <linearGradient id="g" x1="91.59%" x2="66.97%" xlink:href="#f" y1="5.89%" y2="100%"/>
87
- <linearGradient id="h" x1="97.27%" x2="52.53%" xlink:href="#f" y1="6.88%" y2="100%"/>
88
- <linearGradient id="i" x1="82.73%" x2="41.46%" xlink:href="#f" y1="41.06%" y2="167.23%"/>
89
- <linearGradient id="j" x1="49.87%" x2="49.87%" xlink:href="#d" y1="3.62%" y2="100.77%"/>
90
- <linearGradient id="k" x1="100%" x2="72.45%" xlink:href="#f" y1="0%" y2="85.2%"/>
91
- <linearGradient id="l" x1="100%" x2="62.1%" y1="0%" y2="68.86%">
92
- <stop offset="0" stop-color="#163055"/>
93
- <stop offset="1" stop-color="#2f587f"/>
94
- </linearGradient>
95
- <circle id="m" cx="180" cy="102" r="40"/>
96
- <filter id="n" height="340%" width="340%" x="-120%" y="-120%">
97
- <feOffset in="SourceAlpha" result="shadowOffsetOuter1"/>
98
- <feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="32"/>
99
- <feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.696473053 0"/>
100
- </filter>
101
- <linearGradient id="o" x1="0%" y1="50%" y2="50%">
102
- <stop offset="0" stop-color="#fff" stop-opacity="0"/>
103
- <stop offset="1" stop-color="#fff"/>
104
- </linearGradient>
105
- </defs>
106
- <g fill="none" fill-rule="evenodd">
107
- <path d="m0 0h1024v1024h-1024z" fill="url(#a)"/>
108
- <g transform="translate(761 481)">
109
- <path d="m96 27 4-1v11h-4z" fill="#8dbcd2"/>
110
- <path d="m76 23 4-1v15h-4z" fill="#8dbcd2"/>
111
- <g fill="#183352">
112
- <path d="m40 22 4 1v14h-4z"/>
113
- <path d="m20 26 4 1v14h-4z"/>
114
- <path d="m59 0h2v20h-2z" opacity=".5"/>
115
- </g>
116
- <path d="m61 0c3 0 3 2 6 2s3-2 6-2 3 2 6 2v8c-3 0-3-2-6-2s-3 2-6 2-3-2-6-2z" fill="url(#b)"/>
117
- <path d="m50 20 10-2v110h-60l10-100 10-2v10.92l10-.98v-11.94l10-2v12.96l10-.98z" fill="#8dbcd2"/>
118
- <path d="m100 26 10 2 10 100h-60v-110l10 2v13.98l10 .98v-12.96l10 2v11.94l10 .98z" fill="#183352"/>
119
- </g>
120
- <g transform="translate(0 565)">
121
- <path d="m1024 385h-1024v-278.14c118.4 21.09 185.14 57.03 327.4 48.14 198.54-12.4 250-125 500-125 90.18 0 147.92 16.3 196.6 37.12z" fill="url(#c)"/>
122
- <path d="m1024 355h-1024v-275.44c76.46-35.75 137.14-79.56 285-79.56 250 0 301.46 112.6 500 125 103.24 6.45 166.7-10.7 239-28.66z" fill="url(#e)"/>
123
- <path d="m344.12 130.57c23.1 13.47-25.27 81.95-145.12 205.43h649c-344.06-141.7-512.02-210.17-503.88-205.43z" fill="url(#e)"/>
124
- <path d="m0 336v-256.44c76.46-35.75 137.14-79.56 285-79.56 71.14 0 86.22 26.04 32.5 82-48 50 147.33 58.02 36 136.5-40.67 28.67 21.17 67.83 185.5 117.5z" fill="url(#g)"/>
125
- <path d="m317.5 82c-48 50 147.33 58.02 36 136.5-40.67 28.67 21.17 67.83 185.5 117.5h-484z" fill="url(#h)"/>
126
- <path d="m353.5 218.5c-40.67 28.67 21.17 67.83 185.5 117.5h-364z" fill="url(#i)"/>
127
- <path d="m0 459v-194.46c100.25 21.2 167.18 50.29 296.67 42.19 198.57-12.43 250.04-125.15 500.07-125.15 109.75 0 171.47 24.16 227.26 51.25v226.17z" fill="url(#j)"/>
128
- <path d="m1024 459h-177.84c51.95-58.9 48.86-97.16-9.28-114.78-186.64-56.58-101.76-162.64-39.97-162.64 109.64 0 171.34 24.12 227.09 51.19z" fill="url(#k)"/>
129
- <path d="m1024 459h-177.81c52.01-59.01 48.94-97.34-9.22-115l187.03 53.48z" fill="url(#l)"/>
130
- </g>
131
- <g id="moonGroup">
132
- <g transform="translate(94 23)">
133
- <use fill="#000" filter="url(#n)" xlink:href="#m"/>
134
- <use fill="#d2f1fe" xlink:href="#m"/>
135
- </g>
136
- </g>
137
- <g transform="translate(94 23)">
138
- <use fill="#000" filter="url(#n)" xlink:href="#m"/>
139
- <g fill="#fff">
140
- <circle cx="123" cy="255" fill-opacity=".4" r="3"/>
141
- <circle cx="2" cy="234" r="2"/>
142
- <circle cx="33" cy="65" r="3"/>
143
- <circle cx="122" cy="2" r="2"/>
144
- <circle cx="72" cy="144" r="2"/>
145
- <circle cx="282" cy="224" r="2"/>
146
- <circle cx="373" cy="65" opacity=".4" r="3"/>
147
- <circle cx="433" cy="255" r="3"/>
148
- </g>
149
- <path d="m373.25 325.25a5 5 0 0 0 0-10h-75v10z" fill="url(#o)" opacity=".4" transform="matrix(.70710678 .70710678 -.70710678 .70710678 325.523078 -145.38023)"/>
150
- <g fill="#fff">
151
- <circle cx="363" cy="345" r="3"/>
152
- <circle cx="513" cy="115" r="3"/>
153
- <circle cx="723" cy="5" opacity=".4" r="3"/>
154
- <circle cx="422" cy="134" r="2"/>
155
- <circle cx="752" cy="204" r="2"/>
156
- <circle cx="672" cy="114" r="2"/>
157
- <circle cx="853" cy="255" opacity=".4" r="3"/>
158
- <circle cx="623" cy="225" r="3"/>
159
- <circle cx="823" cy="55" r="3"/>
160
- <circle cx="902" cy="144" r="2"/>
161
- <circle cx="552" cy="14" r="2"/>
162
- </g>
163
- </g>
164
- <path d="m796 535a4 4 0 0 1 4 4v20h-8v-20a4 4 0 0 1 4-4z" fill="#486587"/>
165
- <path d="m798 535.54a4 4 0 0 0 -2 3.46v20h-4v-20a4 4 0 0 1 6-3.46zm48-.54a4 4 0 0 1 4 4v20h-8v-20a4 4 0 0 1 4-4z" fill="#071423"/>
166
- <path d="m846 559v-20a4 4 0 0 0 -2-3.46 4 4 0 0 1 6 3.46v20z" fill="#8dbcd2"/>
167
- <path d="m554.67 131.48a9.46 9.46 0 0 1 13.33 0 9.46 9.46 0 0 0 13.33 0l13.33-13.24a28.39 28.39 0 0 1 40 0l10 9.93a14.2 14.2 0 0 0 20 0 14.2 14.2 0 0 1 20 0l.6.6a31.8 31.8 0 0 1 9.4 22.56h-146.66v-3.84c0-6.01 2.4-11.78 6.67-16.01zm196.33-123.23c11.07-11 28.93-11 40 0l10 9.94a14.19 14.19 0 0 0 20 0 14.19 14.19 0 0 1 20 0 16.36 16.36 0 0 0 21.3 1.5l8.7-6.47a33.47 33.47 0 0 1 40 0l4.06 3.03a39.6 39.6 0 0 1 15.94 31.75h-200c0-12.72 8.93-28.75 20-39.75zm-736.9 66.89.9-.9a21.29 21.29 0 0 1 30 0 21.29 21.29 0 0 0 30 0l10-9.93a35.48 35.48 0 0 1 50 0l15 14.9a14.2 14.2 0 0 0 20 0 14.2 14.2 0 0 1 20 0c6.4 6.35 10 15 10 24.02v5.77h-200c0-12.71 5.07-24.9 14.1-33.86z" fill="#fff" opacity=".07" transform="translate(54 301)"/>
168
- </g>
169
- </svg>
51
+ <!-- Right side illustration -->
52
+ <div class="overflow-hidden relative h-64 w-full sm:h-80 lg:h-auto lg:w-1/2">
53
+ <img class="absolute inset-0 object-cover h-full w-full" src="{{ backgroundImage }}" alt="">
170
54
  </div>
171
55
  </div>
172
-
173
- <style>
174
- @keyframes floatMoon {
175
- 0% { transform: translateX(0); }
176
- 50% { transform: translateX(20px); }
177
- 100% { transform: translateX(0); }
178
- }
179
- #moonGroup {
180
- animation: floatMoon 4s ease-in-out infinite;
181
- }
182
-
183
- @keyframes waveTop {
184
- 0% { transform: translateY(0); }
185
- 50% { transform: translateY(-2px); }
186
- 100% { transform: translateY(0); }
187
- }
188
- </style>
@@ -1,5 +1,8 @@
1
1
  <script server>
2
- const backgroundImage = props.backgroundImage || '/images/background-auth.jpg'
2
+ // Laravel 5.7's 404 illustration (MIT), the same scene the framework's own
3
+ // error pages use, so an app that opts into this layout matches them. Pass
4
+ // `backgroundImage` to swap it for anything else.
5
+ const backgroundImage = props.backgroundImage || '/svgs/404.svg'
3
6
  </script>
4
7
 
5
8
  <div class="flex relative justify-center shrink-0 md:px-12 lg:px-0 min-h-full">
@@ -23,7 +26,9 @@ const backgroundImage = props.backgroundImage || '/images/background-auth.jpg'
23
26
  Sorry, we couldn't find the page you're looking for.
24
27
  </p>
25
28
 
26
- <a href="/" class="inline-block mt-10 px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded">
29
+ <!-- Same deep rose as the illustration beside it. White on #b63e59 is
30
+ 5.49:1 and on the #a22a50 hover 7.04:1, so both states clear AA. -->
31
+ <a href="/" class="inline-block mt-10 px-4 py-2 text-white bg-[#b63e59] hover:bg-[#a22a50] rounded">
27
32
  Go back home
28
33
  </a>
29
34
  </main>
@@ -0,0 +1,24 @@
1
+ <script server>
2
+ import { config } from '@stacksjs/config'
3
+
4
+ /**
5
+ * Where a magic sign-in link lands, at `/auth/magic/{token}`.
6
+ *
7
+ * The logic sits in the component, like every other auth page here.
8
+ *
9
+ * It used to sit in a page-level `<script client>` that read a `useRef` from
10
+ * inside a top-level `effect`, and that combination left the page inert: the
11
+ * setup registered, no directive bound, and the page rendered perfectly while
12
+ * its button did nothing. The cause was in stx - `useRef()` returned an object
13
+ * rather than a callable, so `shell()` threw out of the effect and out of the
14
+ * setup function, taking every binding with it. Fixed upstream in stx 0.2.217;
15
+ * the ref is also gone from here, because a template ref is not reactive and
16
+ * reading one during setup only ever answers null.
17
+ *
18
+ * `autoConsume` is passed as an attribute because it is a server decision a
19
+ * client script cannot read.
20
+ */
21
+ const autoConsume = config.auth.magicLink?.confirmInteraction !== true
22
+ </script>
23
+
24
+ <MagicLink autoConsume="{{ autoConsume }}" />
@@ -0,0 +1,42 @@
1
+ <script server>
2
+ // The frontend's not-found page. When no view matches a URL, stx-serve looks
3
+ // for a route at `/404` or `/errors/404` first. Without this file it falls
4
+ // through to its own generic page, which carries none of the app's design
5
+ // (and, on the server `stx build` produces, to the bare string "Not Found").
6
+ //
7
+ // Same split as the framework's production error page in
8
+ // @stacksjs/error-handling, so a 404 looks the same whether the router or the
9
+ // views server answered it. The scene is Laravel 5.7's 404 illustration (MIT),
10
+ // served from public/svgs.
11
+ </script>
12
+
13
+ @title('Page not found')
14
+
15
+ <div class="flex flex-col md:flex-row min-h-screen bg-neutral-50 dark:bg-neutral-900">
16
+ <div class="overflow-hidden md:order-2 h-56 sm:h-72 md:h-auto md:w-1/2 bg-neutral-200 dark:bg-neutral-800">
17
+ <img src="/svgs/404.svg" alt="" class="object-cover h-full w-full">
18
+ </div>
19
+
20
+ <section class="flex md:order-1 flex-1 flex-col items-start justify-center md:flex-none px-8 py-12 md:px-16 md:w-1/2">
21
+ <div class="font-extrabold text-6xl text-neutral-950 tracking-tight md:text-8xl dark:text-white">404</div>
22
+
23
+ <!-- The rule takes the illustration's own deep rose rather than a
24
+ framework accent: it sits inches from the artwork, and a blue mark
25
+ against a warm dusk scene reads as a mistake in any app's brand. The
26
+ dark value is the same ramp's coral, from the scene's night variant. -->
27
+ <div class="my-6 h-1 w-14 bg-[#b63e59] dark:bg-[#ee7566] rounded-full"></div>
28
+
29
+ <h1 class="mb-2 font-semibold text-2xl text-neutral-950 dark:text-white">Page not found</h1>
30
+
31
+ <p class="mb-8 max-w-md text-neutral-500 dark:text-neutral-400">
32
+ The page you are looking for does not exist, or it has moved.
33
+ </p>
34
+
35
+ <a
36
+ href="/"
37
+ class="inline-block px-5 py-2.5 font-medium text-neutral-950 text-sm dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 border border-neutral-300 rounded-lg dark:border-neutral-700"
38
+ >
39
+ Back to home
40
+ </a>
41
+ </section>
42
+ </div>
@@ -0,0 +1,13 @@
1
+ <script server>
2
+ /**
3
+ * The site's forgot-password page, at `/forgot-password` - the address the
4
+ * sign-in card's "Forgot password?" link has always pointed at, and which
5
+ * had nothing behind it outside the dashboard.
6
+ *
7
+ * The container posts to `/password/forgot`, which answers the same neutral
8
+ * 202 whether or not the address has an account, so this page must never
9
+ * report back whether it matched.
10
+ */
11
+ </script>
12
+
13
+ <ForgotPasswordDashboard />
@@ -0,0 +1,68 @@
1
+ <script server>
2
+ /**
3
+ * The site's sign-in page, at `/login`.
4
+ *
5
+ * It lived at `defaults/views/auth/login.stx` and was one line mounting the
6
+ * presentational `Login` component, from a directory no server serves. That
7
+ * component only emits `submit`, so even reachable it would have rendered a
8
+ * form that swallowed every sign-in. The container below is what listens: it
9
+ * calls `useAuth().login`, surfaces the error, and honours `?redirect=`. Its
10
+ * name is where the auth components happen to live, not where it can run.
11
+ *
12
+ * `POST /login` reaches the API without a proxy rule of its own, because the
13
+ * views server forwards every mutating verb to bun-router (DEFAULT_API_METHODS
14
+ * in core/server/src/proxy.ts). GET `/login` is this page.
15
+ *
16
+ * The social buttons go in through the card's `social` slot rather than after
17
+ * it, so they sit with the credentials instead of below the sign-up line.
18
+ * `configuredSocialProviders()` knows each provider's own required config, so
19
+ * a provider missing credentials never reaches a visitor as a button that
20
+ * fails at the redirect. Adding credentials to `config/services.ts` is the
21
+ * whole of what turns one on. Icons come from hugeicons, the collection Stacks
22
+ * defaults to; swap the map if your app standardises on another.
23
+ */
24
+ import { configuredSocialProviders } from '@stacksjs/socials'
25
+
26
+ const PROVIDER_ICONS = {
27
+ apple: 'i-hugeicons-apple',
28
+ facebook: 'i-hugeicons-facebook-01',
29
+ github: 'i-hugeicons-github',
30
+ google: 'i-hugeicons-google',
31
+ twitter: 'i-hugeicons-new-twitter',
32
+ }
33
+
34
+ const socialSignIn = configuredSocialProviders().map(provider => ({
35
+ label: provider.label,
36
+ href: `/auth/${provider.name}/redirect`,
37
+ icon: PROVIDER_ICONS[provider.name] ?? 'i-hugeicons-user-circle',
38
+ }))
39
+ </script>
40
+
41
+ <LoginDashboard>
42
+ <template #social>
43
+ @if (socialSignIn.length)
44
+ <div class="mt-6">
45
+ <div class="relative">
46
+ <div class="flex absolute inset-0 items-center" aria-hidden="true">
47
+ <div class="w-full border-neutral-200 border-t dark:border-neutral-700"></div>
48
+ </div>
49
+ <div class="flex relative justify-center text-sm">
50
+ <span class="px-4 text-neutral-500 dark:text-neutral-400 bg-white dark:bg-neutral-800">Or continue with</span>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="grid gap-3 mt-6 {{ socialSignIn.length > 1 ? 'grid-cols-2' : 'grid-cols-1' }}">
55
+ @foreach (socialSignIn as provider)
56
+ <a
57
+ href="{{ provider.href }}"
58
+ class="flex gap-2 items-center justify-center px-3 py-2 font-medium text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-700 border border-neutral-300 rounded-lg dark:border-neutral-600"
59
+ >
60
+ <i class="h-5 w-5 {{ provider.icon }}" aria-hidden="true"></i>
61
+ <span>{{ provider.label }}</span>
62
+ </a>
63
+ @endforeach
64
+ </div>
65
+ </div>
66
+ @endif
67
+ </template>
68
+ </LoginDashboard>
@@ -0,0 +1,18 @@
1
+ <script server>
2
+ /**
3
+ * Where a mailed password-reset link lands, at `/password/reset/{token}`.
4
+ *
5
+ * The framework has been sending people here for as long as it has had a
6
+ * reset mailer - `core/auth/src/password/reset.ts` builds the link from
7
+ * `config.auth.passwordReset.url`, whose default is
8
+ * `/password/reset/{token}?email={email}` - and nothing has ever served the
9
+ * address. Every reset email ended at a 404, which made the whole flow
10
+ * (the send endpoint, the token store, the reset action, the mail template)
11
+ * unreachable from a browser.
12
+ *
13
+ * An app that points `config.auth.passwordReset.url` somewhere else is free
14
+ * to keep its own page; this is the page the default link needs.
15
+ */
16
+ </script>
17
+
18
+ <ResetPasswordDashboard />
@@ -0,0 +1,64 @@
1
+ <script server>
2
+ /**
3
+ * The site's sign-up page, at `/register`.
4
+ *
5
+ * It lived at `defaults/views/auth/signup.stx`, in a directory no server
6
+ * serves, and carried its own hand-written form whose `action="#"` posted
7
+ * nowhere. It also declared `id="password"` twice and named the confirmation
8
+ * field `password` as well, so the two inputs collided. The container below is
9
+ * what actually registers: it calls `useAuth().register`, surfaces the error,
10
+ * and honours `?redirect=`. Its name is where the auth components happen to
11
+ * live, not where it can run.
12
+ *
13
+ * The social block is what that page did get right, kept here and moved inside
14
+ * the card through its `social` slot. `configuredSocialProviders()` knows each
15
+ * provider's own required config (Apple has no clientSecret and is validated
16
+ * on its signing key instead), so what renders is exactly the set that will
17
+ * complete a sign-in. Adding credentials to `config/services.ts` is the whole
18
+ * of what turns one on.
19
+ */
20
+ import { configuredSocialProviders } from '@stacksjs/socials'
21
+
22
+ const PROVIDER_ICONS = {
23
+ apple: 'i-hugeicons-apple',
24
+ facebook: 'i-hugeicons-facebook-01',
25
+ github: 'i-hugeicons-github',
26
+ google: 'i-hugeicons-google',
27
+ twitter: 'i-hugeicons-new-twitter',
28
+ }
29
+
30
+ const socialSignUp = configuredSocialProviders().map(provider => ({
31
+ label: provider.label,
32
+ href: `/auth/${provider.name}/redirect`,
33
+ icon: PROVIDER_ICONS[provider.name] ?? 'i-hugeicons-user-circle',
34
+ }))
35
+ </script>
36
+
37
+ <RegisterDashboard>
38
+ <template #social>
39
+ @if (socialSignUp.length)
40
+ <div class="mt-6">
41
+ <div class="relative">
42
+ <div class="flex absolute inset-0 items-center" aria-hidden="true">
43
+ <div class="w-full border-neutral-200 border-t dark:border-neutral-700"></div>
44
+ </div>
45
+ <div class="flex relative justify-center text-sm">
46
+ <span class="px-4 text-neutral-500 dark:text-neutral-400 bg-white dark:bg-neutral-800">Or continue with</span>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="grid gap-3 mt-6 {{ socialSignUp.length > 1 ? 'grid-cols-2' : 'grid-cols-1' }}">
51
+ @foreach (socialSignUp as provider)
52
+ <a
53
+ href="{{ provider.href }}"
54
+ class="flex gap-2 items-center justify-center px-3 py-2 font-medium text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-700 border border-neutral-300 rounded-lg dark:border-neutral-600"
55
+ >
56
+ <i class="h-5 w-5 {{ provider.icon }}" aria-hidden="true"></i>
57
+ <span>{{ provider.label }}</span>
58
+ </a>
59
+ @endforeach
60
+ </div>
61
+ </div>
62
+ @endif
63
+ </template>
64
+ </RegisterDashboard>
@@ -9,6 +9,9 @@ on:
9
9
  branches:
10
10
  - main
11
11
 
12
+ # So a maintainer can ship without pushing an empty commit.
13
+ workflow_dispatch:
14
+
12
15
  concurrency:
13
16
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
14
17
  cancel-in-progress: true
@@ -119,3 +122,87 @@ jobs:
119
122
 
120
123
  - name: Test Suite
121
124
  run: bun run test
125
+
126
+ # Push to deploy: `buddy deploy` on every green push to main.
127
+ #
128
+ # A fresh app has nothing to deploy to, and red CI on day one is worse than
129
+ # no deploy, so this checks for its secrets first and finishes green with a
130
+ # notice when they are missing. Add them under
131
+ # Settings > Secrets and variables > Actions to turn it on:
132
+ #
133
+ # DOTENV_PRIVATE_KEY_PRODUCTION decrypts .env.production for the deploy
134
+ # DEPLOY_SSH_KEY private key for the target server
135
+ #
136
+ # Cloud tokens are optional. Without them the release still ships and DNS is
137
+ # still published; only the provider-specific parts are skipped.
138
+ deploy:
139
+ if: >-
140
+ github.event_name == 'workflow_dispatch' ||
141
+ (github.event_name == 'push' && github.ref == 'refs/heads/main')
142
+ needs: [lint, typecheck, test]
143
+ runs-on: ubuntu-latest
144
+ timeout-minutes: 30
145
+ permissions:
146
+ contents: read
147
+ deployments: write
148
+
149
+ steps:
150
+ - name: Checkout Code
151
+ uses: actions/checkout@v4
152
+
153
+ - name: Is deployment configured?
154
+ id: check
155
+ env:
156
+ DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
157
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
158
+ run: |
159
+ if [ -n "$DOTENV_PRIVATE_KEY_PRODUCTION" ] && [ -n "$DEPLOY_SSH_KEY" ]; then
160
+ echo 'configured=true' >> "$GITHUB_OUTPUT"
161
+ else
162
+ echo 'configured=false' >> "$GITHUB_OUTPUT"
163
+ echo '::notice::Deployment is not configured, so this step did nothing. Add DOTENV_PRIVATE_KEY_PRODUCTION and DEPLOY_SSH_KEY to deploy on every push to main.'
164
+ fi
165
+
166
+ - name: Setup Pantry
167
+ if: steps.check.outputs.configured == 'true'
168
+ uses: pantry-pm/pantry/packages/action@c1417d98f5613290bc3b1f96d35086b834684659 # v0.10.47 + the socket-hang-up retry fix
169
+ with:
170
+ install: 'false'
171
+
172
+ - name: Use cached node_modules
173
+ if: steps.check.outputs.configured == 'true'
174
+ uses: actions/cache@v4
175
+ with:
176
+ path: node_modules
177
+ key: node-modules-${{ hashFiles('**/bun.lock') }}
178
+ restore-keys: |
179
+ node-modules-
180
+
181
+ - name: Install Dependencies
182
+ if: steps.check.outputs.configured == 'true'
183
+ run: bun install
184
+
185
+ - name: Prepare Deploy SSH Key
186
+ if: steps.check.outputs.configured == 'true'
187
+ env:
188
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
189
+ run: |
190
+ mkdir -p ~/.ssh
191
+ chmod 700 ~/.ssh
192
+ printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
193
+ chmod 600 ~/.ssh/id_ed25519
194
+ ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub
195
+ chmod 644 ~/.ssh/id_ed25519.pub
196
+
197
+ - name: Deploy production
198
+ if: steps.check.outputs.configured == 'true'
199
+ run: bun buddy deploy --yes
200
+ env:
201
+ APP_ENV: production
202
+ DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
203
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
204
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
205
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
206
+ # macOS resource forks in a tarball built on a developer machine make
207
+ # the extraction on the server noisy and occasionally fatal.
208
+ COPYFILE_DISABLE: '1'
@@ -1,7 +0,0 @@
1
- <script>
2
- const { t } = useI18n()
3
- </script>
4
-
5
- <div>
6
- {{ t('not-found') }}
7
- </div>
package/views/about.md DELETED
@@ -1,3 +0,0 @@
1
- ---
2
- title: About
3
- ---
@@ -1 +0,0 @@
1
- <Login />
@@ -1,88 +0,0 @@
1
- <script server>
2
- import { config } from '@stacksjs/config'
3
-
4
- /**
5
- * The magic-link interstitial. The email's GET lands here and this page
6
- * POSTs the token to the consume endpoint - a GET must never consume,
7
- * because mail-security scanners prefetch links and would burn the
8
- * single-use token before the person ever taps it.
9
- *
10
- * `confirmInteraction` renders a button instead of auto-submitting, for
11
- * environments whose scanners execute page scripts too.
12
- */
13
- const autoConsume = config.auth.magicLink?.confirmInteraction !== true
14
- </script>
15
-
16
- <script client>
17
- const route = useRoute()
18
- const status = state('ready') // ready | working | done | failed
19
- const message = state('')
20
- const shell = useRef('shell')
21
-
22
- async function consume() {
23
- if (status() === 'working' || status() === 'done')
24
- return
25
-
26
- status.set('working')
27
- try {
28
- const reply = await fetch('/auth/magic-link/consume', {
29
- method: 'POST',
30
- headers: { 'Content-Type': 'application/json' },
31
- credentials: 'same-origin',
32
- body: JSON.stringify({ token: String(route.params.token || '') }),
33
- })
34
- const body = await reply.json().catch(() => ({}))
35
-
36
- if (!reply.ok) {
37
- status.set('failed')
38
- message.set(body.message || body.error || 'That sign-in link is not valid.')
39
- return
40
- }
41
-
42
- status.set('done')
43
- const redirect = String(body.redirect_to || '/')
44
- navigate(redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/')
45
- }
46
- catch {
47
- status.set('failed')
48
- message.set('Something went wrong. Request a new link and try again.')
49
- }
50
- }
51
-
52
- effect(() => {
53
- // Auto-consume only when the server said so - the flag rides a data
54
- // attribute because client scripts cannot read server config directly.
55
- if (shell() && shell().dataset.autoConsume === 'true')
56
- consume()
57
- })
58
- </script>
59
-
60
- <template>
61
- <main ref="shell" data-auto-consume="{{ autoConsume }}" class="min-h-[100dvh] flex items-center justify-center px-6">
62
- <div class="w-full max-w-sm text-center">
63
- <div :if="status() === 'failed'">
64
- <div class="mx-auto h-12 w-12 rounded-full bg-red-50 flex items-center justify-center">
65
- <span class="h-6 w-6 text-red-500 i-hugeicons-alert-02"></span>
66
- </div>
67
- <h1 class="mt-5 text-xl font-semibold tracking-tight">Sign-in link problem</h1>
68
- <p class="mt-2 text-[15px] text-gray-600">{{ message() }}</p>
69
- <a href="/login" class="mt-6 inline-block rounded-lg px-5 py-2.5 font-medium bg-gray-900 text-white">Back to sign in</a>
70
- </div>
71
- <div :else-if="status() === 'working' || status() === 'done'">
72
- <div class="mx-auto h-12 w-12 rounded-full bg-gray-100 flex items-center justify-center">
73
- <span class="h-6 w-6 text-gray-500 animate-spin i-hugeicons-loading-03"></span>
74
- </div>
75
- <h1 class="mt-5 text-xl font-semibold tracking-tight">Signing you in</h1>
76
- <p class="mt-2 text-[15px] text-gray-600">One moment.</p>
77
- </div>
78
- <div :else>
79
- <div class="mx-auto h-12 w-12 rounded-full bg-gray-100 flex items-center justify-center">
80
- <span class="h-6 w-6 text-gray-600 i-hugeicons-mail-open-01"></span>
81
- </div>
82
- <h1 class="mt-5 text-xl font-semibold tracking-tight">Continue to sign in</h1>
83
- <p class="mt-2 text-[15px] text-gray-600">Tap below to finish signing in with your emailed link.</p>
84
- <button type="button" @click="consume" class="mt-6 rounded-lg px-6 py-3 font-medium bg-gray-900 text-white transition-transform active:scale-[0.98]">Continue</button>
85
- </div>
86
- </div>
87
- </main>
88
- </template>