create-zerotal 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +1 -1
  3. package/src/prompts.ts +6 -6
  4. package/src/scaffold.ts +9 -3
  5. package/templates/admin/README.md +41 -0
  6. package/templates/admin/_env.example +17 -0
  7. package/templates/admin/app/admin/index.ts +6 -0
  8. package/templates/admin/app/auth/passwords.ts +79 -0
  9. package/templates/admin/config/database.ts +4 -3
  10. package/templates/admin/database/migrations/0001_create_password_reset_tokens_table.ts +26 -0
  11. package/templates/admin/package.json +9 -1
  12. package/templates/admin/tsconfig.json +7 -2
  13. package/templates/api/README.md +39 -0
  14. package/templates/api/_env.example +13 -0
  15. package/templates/api/app/controllers/AuthController.ts +11 -8
  16. package/templates/api/app/controllers/WelcomeController.ts +1 -1
  17. package/templates/api/app/middleware/RequireAuth.ts +1 -1
  18. package/templates/api/app/models/User.ts +2 -2
  19. package/templates/api/bootstrap/app.ts +6 -6
  20. package/templates/api/config/app.ts +1 -1
  21. package/templates/api/config/database.ts +12 -3
  22. package/templates/api/config/notifications.ts +1 -1
  23. package/templates/api/config/queue.ts +1 -1
  24. package/templates/api/config/session.ts +2 -2
  25. package/templates/api/database/migrations/0001_create_users_table.ts +8 -8
  26. package/templates/api/package.json +9 -1
  27. package/templates/api/routes/index.ts +23 -8
  28. package/templates/api/tests/auth.test.ts +6 -3
  29. package/templates/api/tests/helpers.ts +6 -6
  30. package/templates/api/tsconfig.json +7 -3
  31. package/templates/flow/README.md +39 -0
  32. package/templates/flow/_env.example +17 -0
  33. package/templates/flow/app/auth/passwords.ts +79 -0
  34. package/templates/flow/app/flow/layouts/app.tsx +10 -1
  35. package/templates/flow/app/flow/pages/forgot-password.tsx +69 -0
  36. package/templates/flow/app/flow/pages/login.tsx +88 -0
  37. package/templates/flow/app/flow/pages/profile.tsx +194 -0
  38. package/templates/flow/app/flow/pages/register.tsx +120 -0
  39. package/templates/flow/app/flow/pages/reset-password.tsx +103 -0
  40. package/templates/flow/app/flow/ui.ts +27 -0
  41. package/templates/flow/app/models/User.ts +17 -0
  42. package/templates/flow/bootstrap/app.ts +8 -0
  43. package/templates/flow/bootstrap/providers.ts +6 -1
  44. package/templates/flow/config/auth.ts +7 -0
  45. package/templates/flow/config/database.ts +18 -0
  46. package/templates/flow/config/session.ts +14 -0
  47. package/templates/flow/database/migrations/0001_create_users_table.ts +18 -0
  48. package/templates/flow/database/migrations/0002_create_password_reset_tokens_table.ts +23 -0
  49. package/templates/flow/package.json +10 -4
  50. package/templates/flow/tests/smoke.test.ts +21 -1
  51. package/templates/flow/tsconfig.json +10 -3
  52. package/templates/minimal/README.md +39 -0
  53. package/templates/minimal/_env.example +9 -0
  54. package/templates/minimal/bootstrap/app.ts +2 -2
  55. package/templates/minimal/package.json +10 -5
  56. package/templates/minimal/tests/smoke.test.ts +1 -1
  57. package/templates/minimal/tsconfig.json +13 -4
  58. package/templates/react/README.md +40 -0
  59. package/templates/react/_env.example +18 -0
  60. package/templates/react/app/auth/passwords.ts +79 -0
  61. package/templates/react/app/models/User.ts +17 -0
  62. package/templates/react/app/routes/forgot-password.ts +24 -0
  63. package/templates/react/app/routes/login.ts +32 -0
  64. package/templates/react/app/routes/logout.ts +12 -0
  65. package/templates/react/app/routes/profile/password.ts +30 -0
  66. package/templates/react/app/routes/profile.ts +39 -0
  67. package/templates/react/app/routes/register.ts +39 -0
  68. package/templates/react/app/routes/reset-password.ts +37 -0
  69. package/templates/react/bootstrap/app.ts +8 -0
  70. package/templates/react/bootstrap/providers.ts +7 -4
  71. package/templates/react/config/auth.ts +7 -0
  72. package/templates/react/config/database.ts +18 -0
  73. package/templates/react/database/migrations/0001_create_users_table.ts +18 -0
  74. package/templates/react/database/migrations/0002_create_password_reset_tokens_table.ts +23 -0
  75. package/templates/react/package.json +8 -2
  76. package/templates/react/resources/js/Layouts/AppLayout.tsx +12 -2
  77. package/templates/react/resources/js/pages/forgot-password.tsx +65 -0
  78. package/templates/react/resources/js/pages/login.tsx +92 -0
  79. package/templates/react/resources/js/pages/profile.tsx +147 -0
  80. package/templates/react/resources/js/pages/register.tsx +101 -0
  81. package/templates/react/resources/js/pages/reset-password.tsx +80 -0
  82. package/templates/react/resources/js/pages.generated.ts +5 -0
  83. package/templates/react/tests/smoke.test.ts +58 -2
  84. package/templates/react/tsconfig.json +10 -3
  85. package/templates/vue/README.md +40 -0
  86. package/templates/vue/_env.example +18 -0
  87. package/templates/vue/app/auth/passwords.ts +79 -0
  88. package/templates/vue/app/exceptions/Handler.ts +55 -0
  89. package/templates/vue/app/models/User.ts +17 -0
  90. package/templates/vue/app/routes/forgot-password.ts +24 -0
  91. package/templates/vue/app/routes/login.ts +32 -0
  92. package/templates/vue/app/routes/logout.ts +12 -0
  93. package/templates/vue/app/routes/profile/password.ts +30 -0
  94. package/templates/vue/app/routes/profile.ts +39 -0
  95. package/templates/vue/app/routes/register.ts +39 -0
  96. package/templates/vue/app/routes/reset-password.ts +37 -0
  97. package/templates/vue/bootstrap/app.ts +15 -3
  98. package/templates/vue/bootstrap/providers.ts +8 -1
  99. package/templates/vue/config/auth.ts +7 -0
  100. package/templates/vue/config/database.ts +18 -0
  101. package/templates/vue/config/session.ts +11 -0
  102. package/templates/vue/database/migrations/0001_create_users_table.ts +18 -0
  103. package/templates/vue/database/migrations/0002_create_password_reset_tokens_table.ts +23 -0
  104. package/templates/vue/package.json +8 -2
  105. package/templates/vue/public/zt.svg +17 -0
  106. package/templates/vue/resources/app.html +27 -1
  107. package/templates/vue/resources/css/app.css +205 -0
  108. package/templates/vue/resources/js/Components/Button.vue +49 -0
  109. package/templates/vue/resources/js/Components/Card.vue +23 -0
  110. package/templates/vue/resources/js/Components/FlashToasts.vue +81 -0
  111. package/templates/vue/resources/js/Components/Icon.vue +40 -0
  112. package/templates/vue/resources/js/Components/TextField.vue +66 -0
  113. package/templates/vue/resources/js/Components/ThemeToggle.vue +46 -0
  114. package/templates/vue/resources/js/Layouts/AppLayout.vue +140 -25
  115. package/templates/vue/resources/js/lib/cn.ts +13 -0
  116. package/templates/vue/resources/js/lib/site.ts +11 -0
  117. package/templates/vue/resources/js/pages/error.vue +47 -0
  118. package/templates/vue/resources/js/pages/forgot-password.vue +47 -0
  119. package/templates/vue/resources/js/pages/login.vue +67 -0
  120. package/templates/vue/resources/js/pages/profile.vue +119 -0
  121. package/templates/vue/resources/js/pages/register.vue +77 -0
  122. package/templates/vue/resources/js/pages/reset-password.vue +58 -0
  123. package/templates/vue/resources/js/pages.generated.ts +10 -4
  124. package/templates/vue/resources/js/types.ts +17 -0
  125. package/templates/vue/tests/smoke.test.ts +50 -1
  126. package/templates/vue/tsconfig.json +10 -3
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { Head, Link, useForm, usePage } from "@inertiajs/vue3";
3
+ import AppLayout from "../Layouts/AppLayout.vue";
4
+ import Button from "../Components/Button.vue";
5
+ import Card from "../Components/Card.vue";
6
+ import TextField from "../Components/TextField.vue";
7
+ import type { SharedProps } from "../types";
8
+
9
+ defineOptions({ layout: AppLayout });
10
+ defineProps<{ title: string }>();
11
+
12
+ const old = usePage<SharedProps>().props.old;
13
+
14
+ const form = useForm({
15
+ name: typeof old["name"] === "string" ? old["name"] : "",
16
+ email: typeof old["email"] === "string" ? old["email"] : "",
17
+ password: "",
18
+ password_confirmation: "",
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <Head :title="title" />
24
+
25
+ <div class="mx-auto max-w-md">
26
+ <header>
27
+ <h1 class="text-3xl font-bold tracking-tight text-balance">{{ title }}</h1>
28
+ <p class="mt-2 text-muted-foreground">It takes about ten seconds.</p>
29
+ </header>
30
+
31
+ <Card class="mt-8 p-6 sm:p-8">
32
+ <form class="space-y-5" @submit.prevent="form.post('/register')">
33
+ <TextField
34
+ v-model="form.name"
35
+ label="Name"
36
+ autocomplete="name"
37
+ :error="form.errors.name"
38
+ required
39
+ />
40
+ <TextField
41
+ v-model="form.email"
42
+ label="Email"
43
+ type="email"
44
+ autocomplete="email"
45
+ :error="form.errors.email"
46
+ required
47
+ />
48
+ <TextField
49
+ v-model="form.password"
50
+ label="Password"
51
+ type="password"
52
+ autocomplete="new-password"
53
+ hint="At least 8 characters."
54
+ :error="form.errors.password"
55
+ required
56
+ />
57
+ <TextField
58
+ v-model="form.password_confirmation"
59
+ label="Confirm password"
60
+ type="password"
61
+ autocomplete="new-password"
62
+ :error="form.errors.password_confirmation"
63
+ required
64
+ />
65
+
66
+ <Button type="submit" :disabled="form.processing">
67
+ {{ form.processing ? "Creating…" : "Create account" }}
68
+ </Button>
69
+ </form>
70
+ </Card>
71
+
72
+ <p class="mt-6 text-sm text-muted-foreground">
73
+ Already registered?
74
+ <Link href="/login" class="text-accent hover:underline">Sign in</Link>
75
+ </p>
76
+ </div>
77
+ </template>
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { Head, useForm } from "@inertiajs/vue3";
3
+ import AppLayout from "../Layouts/AppLayout.vue";
4
+ import Button from "../Components/Button.vue";
5
+ import Card from "../Components/Card.vue";
6
+ import TextField from "../Components/TextField.vue";
7
+
8
+ defineOptions({ layout: AppLayout });
9
+
10
+ // Token and email arrive on the query string of the emailed link and ride along
11
+ // in the form, so the POST carries everything the server needs to verify it
12
+ // without trusting the session.
13
+ const props = defineProps<{ title: string; token: string; email: string }>();
14
+
15
+ const form = useForm({
16
+ token: props.token,
17
+ email: props.email,
18
+ password: "",
19
+ password_confirmation: "",
20
+ });
21
+ </script>
22
+
23
+ <template>
24
+ <Head :title="title" />
25
+
26
+ <div class="mx-auto max-w-md">
27
+ <header>
28
+ <h1 class="text-3xl font-bold tracking-tight text-balance">{{ title }}</h1>
29
+ <p class="mt-2 text-muted-foreground">Resetting the password for {{ email }}.</p>
30
+ </header>
31
+
32
+ <Card class="mt-8 p-6 sm:p-8">
33
+ <form class="space-y-5" @submit.prevent="form.post('/reset-password')">
34
+ <TextField
35
+ v-model="form.password"
36
+ label="New password"
37
+ type="password"
38
+ autocomplete="new-password"
39
+ hint="At least 8 characters."
40
+ :error="form.errors.password"
41
+ required
42
+ />
43
+ <TextField
44
+ v-model="form.password_confirmation"
45
+ label="Confirm password"
46
+ type="password"
47
+ autocomplete="new-password"
48
+ :error="form.errors.password_confirmation"
49
+ required
50
+ />
51
+
52
+ <Button type="submit" :disabled="form.processing">
53
+ {{ form.processing ? "Updating…" : "Update password" }}
54
+ </Button>
55
+ </form>
56
+ </Card>
57
+ </div>
58
+ </template>
@@ -6,7 +6,13 @@
6
6
  // Converting to static imports will bundle ALL pages into app.js.
7
7
 
8
8
  export const pages: Record<string, () => Promise<{ default: unknown }>> = {
9
- 'about': () => import('./pages/about.vue'),
10
- 'contact': () => import('./pages/contact.vue'),
11
- 'home': () => import('./pages/home.vue'),
12
- };
9
+ about: () => import("./pages/about.vue"),
10
+ contact: () => import("./pages/contact.vue"),
11
+ error: () => import("./pages/error.vue"),
12
+ "forgot-password": () => import("./pages/forgot-password.vue"),
13
+ home: () => import("./pages/home.vue"),
14
+ login: () => import("./pages/login.vue"),
15
+ profile: () => import("./pages/profile.vue"),
16
+ register: () => import("./pages/register.vue"),
17
+ "reset-password": () => import("./pages/reset-password.vue"),
18
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Props `@zerotal/inertia` merges into every page, so no controller has to pass
3
+ * them. Type `usePage<SharedProps>()` with this to read them safely.
4
+ *
5
+ * Add your own here after registering them with `Inertia.share(...)`.
6
+ */
7
+ export interface SharedProps {
8
+ /** The signed-in user, or null. Populated once an auth provider is registered. */
9
+ auth: { user: { id: number; name: string; email: string } | null };
10
+ /** One-request-only messages set with `http.flash("success" | "error", …)`. */
11
+ flash: { success: string | null; error: string | null };
12
+ /** Field errors from a failed `validate()` call, keyed by field name. */
13
+ errors: Record<string, string>;
14
+ /** Submitted input from a failed `validate()`, so a form can repopulate itself. */
15
+ old: Record<string, unknown>;
16
+ [key: string]: unknown;
17
+ }
@@ -7,7 +7,7 @@
7
7
  * you have real tests, or keep it as the shape to copy.
8
8
  */
9
9
  import { beforeAll, afterAll, describe, test, expect } from 'bun:test';
10
- import { createTestApp, type TestApp } from '@zerotal/testing';
10
+ import { createTestApp, type TestApp } from 'zerotal/testing';
11
11
 
12
12
  let app: TestApp;
13
13
 
@@ -15,6 +15,8 @@ beforeAll(async () => {
15
15
  // Session encryption needs a key. Seed a deterministic one for tests; `??=`
16
16
  // leaves a real key alone when the app's .env is present.
17
17
  Bun.env.APP_KEY ??= 'test-app-key-aaaaaaaaaaaaaaaaaaaaaaaa';
18
+ // Each run gets its own throwaway schema rather than the dev database.
19
+ Bun.env.ZT_DB_URL ??= ':memory:';
18
20
  app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
19
21
  });
20
22
 
@@ -44,3 +46,50 @@ describe('scaffold', () => {
44
46
  (await app.get('/definitely-not-a-route')).assertNotFound();
45
47
  });
46
48
  });
49
+
50
+ /**
51
+ * The auth surface, end to end. Registration is the round trip worth asserting:
52
+ * a valid post creates the account, signs the visitor in, and lands them on a
53
+ * page a guest cannot reach — which proves the guard and the session together.
54
+ */
55
+ describe('auth', () => {
56
+ test('serves the guest screens', async () => {
57
+ (await app.get('/login')).assertInertia('login');
58
+ (await app.get('/register')).assertInertia('register');
59
+ (await app.get('/forgot-password')).assertInertia('forgot-password');
60
+ });
61
+
62
+ test('keeps the profile behind a sign-in', async () => {
63
+ (await app.get('/profile')).assertRedirect('/login');
64
+ });
65
+
66
+ test('rejects a registration whose confirmation does not match', async () => {
67
+ const res = await app.postForm(
68
+ '/register',
69
+ {
70
+ name: 'Ada Lovelace',
71
+ email: 'mismatch@example.com',
72
+ password: 'correct-horse-battery',
73
+ password_confirmation: 'something-else',
74
+ },
75
+ { Referer: `${app.baseUrl}/register` },
76
+ );
77
+
78
+ res.assertRedirect('/register');
79
+ res.assertInvalid(['password']);
80
+ });
81
+
82
+ test('registers a visitor and signs them in', async () => {
83
+ const res = await app.postForm('/register', {
84
+ name: 'Ada Lovelace',
85
+ email: 'ada@example.com',
86
+ password: 'correct-horse-battery',
87
+ password_confirmation: 'correct-horse-battery',
88
+ });
89
+
90
+ // Landing on /profile is the proof: it is a guarded route, so reaching it
91
+ // means the account was created *and* the session was established.
92
+ res.assertRedirect('/profile');
93
+ });
94
+
95
+ });
@@ -13,9 +13,16 @@
13
13
  "jsxImportSource": "vue",
14
14
  "skipLibCheck": true,
15
15
  "paths": {
16
- "@app/*": ["./app/*"],
17
- "@pages/*": ["./resources/js/pages/*"]
16
+ "@app/*": [
17
+ "./app/*"
18
+ ],
19
+ "@pages/*": [
20
+ "./resources/js/pages/*"
21
+ ]
18
22
  }
19
23
  },
20
- "exclude": ["node_modules", ".zerotal"]
24
+ "exclude": [
25
+ "node_modules",
26
+ ".zerotal"
27
+ ]
21
28
  }