@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.
- package/ai/skills/stacks-browse/scripts/browse.ts +63 -4
- package/ide/vscode/package.json +1 -1
- package/lang/de.yml +0 -1
- package/package.json +7 -2
- package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +8 -0
- package/resources/components/Dashboard/Auth/Login.stx +8 -0
- package/resources/components/Dashboard/Auth/LoginDashboard.stx +5 -1
- package/resources/components/Dashboard/Auth/MagicLink.stx +94 -0
- package/resources/components/Dashboard/Auth/Register.stx +7 -0
- package/resources/components/Dashboard/Auth/RegisterDashboard.stx +5 -1
- package/resources/components/Dashboard/Auth/ResetPassword.stx +163 -0
- package/resources/components/Dashboard/Auth/ResetPasswordDashboard.stx +95 -0
- package/resources/layouts/403.stx +15 -148
- package/resources/layouts/404.stx +7 -2
- package/resources/views/auth/magic/[token].stx +24 -0
- package/resources/views/errors/404.stx +42 -0
- package/resources/views/forgot-password.stx +13 -0
- package/resources/views/login.stx +68 -0
- package/resources/views/password/reset/[token].stx +18 -0
- package/resources/views/register.stx +64 -0
- package/vcs/github/workflows/ci.yml +87 -0
- package/views/[...all].stx +0 -7
- package/views/about.md +0 -3
- package/views/auth/login.stx +0 -1
- package/views/auth/magic/[token].stx +0 -88
- package/views/auth/signup.stx +0 -120
- package/views/forum/category.stx +0 -92
- package/views/forum/index.stx +0 -209
- package/views/forum/new-topic.stx +0 -113
- package/views/forum/profile.stx +0 -1
- package/views/forum/topic.stx +0 -143
- package/views/hello-world/[name].stx +0 -47
- 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
|
|
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
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
>
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
58
|
-
<div class="
|
|
59
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
package/views/[...all].stx
DELETED
package/views/about.md
DELETED
package/views/auth/login.stx
DELETED
|
@@ -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>
|