create-zerotal 1.0.4 → 1.1.0
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/CHANGELOG.md +10 -1
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/scaffold.ts +10 -4
- package/templates/admin/app/models/Product.ts +4 -4
- package/templates/admin/app/models/Setting.ts +2 -2
- package/templates/admin/app/models/User.ts +2 -2
- package/templates/admin/bootstrap/providers.ts +14 -0
- package/templates/admin/package.json +1 -0
- package/templates/api/app/models/User.ts +1 -1
- package/templates/api/bootstrap/app.ts +14 -0
- package/templates/api/package.json +1 -0
- package/templates/flow/app/flow/pages/register.tsx +9 -1
- package/templates/flow/app/models/User.ts +16 -3
- package/templates/flow/bootstrap/providers.ts +21 -1
- package/templates/flow/config/database.ts +6 -4
- package/templates/flow/package.json +1 -0
- package/templates/flow/tests/{smoke.test.ts → smoke.test.ts.tmpl} +49 -1
- package/templates/minimal/bootstrap/app.ts +11 -1
- package/templates/minimal/package.json +1 -0
- package/templates/react/app/models/User.ts +16 -3
- package/templates/react/app/routes/register.ts +10 -1
- package/templates/react/bootstrap/providers.ts +21 -1
- package/templates/react/config/database.ts +6 -4
- package/templates/react/package.json +1 -0
- package/templates/react/tests/{smoke.test.ts → smoke.test.ts.tmpl} +7 -1
- package/templates/vue/app/models/User.ts +1 -1
- package/templates/vue/bootstrap/providers.ts +1 -0
- package/templates/vue/config/database.ts +6 -4
- package/templates/vue/tests/{smoke.test.ts → smoke.test.ts.tmpl} +7 -1
- /package/templates/admin/tests/{admin_test.ts → admin.test.ts.tmpl} +0 -0
- /package/templates/api/tests/{auth.test.ts → auth.test.ts.tmpl} +0 -0
- /package/templates/minimal/tests/{smoke.test.ts → smoke.test.ts.tmpl} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Changelog
|
|
1
|
+
# Changelog — @zerotal/create-zerotal
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here. The format is
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/); this package
|
|
@@ -8,6 +8,15 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.1.0] — 2026-08-08
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`StorageProvider` is registered in the scaffold.** `Storage.disk(...)` is used directly in the docs but the facade was unbound, so the first call failed with "Facade [storage] is not bound in the container". The error names the fix precisely; the cost is *when* it appears — a storage call sits behind auth, permission and upload checks, so simpler probes return 401/403 long before reaching it, and the first real upload is often in production. The provider falls back to `StorageConfig()` defaults and mounts no middleware when no disk is servable, so registering it unused costs nothing.
|
|
16
|
+
- **A fresh scaffold can run `migrate`.** The flow, react and vue templates shipped `synchronize` on *and* baseline migrations, so boot-time sync created `users` from the model and the first `bun zt migrate` collided with it — on the very first command a new user types. Migrations are now the single source of truth in those templates, as they already were in `api`, and `bun zt migrate` is listed in the scaffold's next steps.
|
|
17
|
+
- **The scaffolded test suite builds its schema.** With `synchronize` off, `createTestApp()` hands back a `:memory:` database with no tables; the smoke test now calls `migrateDatabase()`, which also means the schema under test is the schema that ships.
|
|
18
|
+
- **Nullable model fields can be cleared.** The templates declared them `?: T` under `exactOptionalPropertyTypes: true`, which means "may be absent, but never `undefined`" — so assigning `undefined` to clear the field, which is the point of a nullable column, did not typecheck. They are now `?: T | undefined`.
|
|
19
|
+
|
|
11
20
|
## [1.0.4] — 2026-08-07
|
|
12
21
|
|
|
13
22
|
### Fixed
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -110,7 +110,9 @@ async function main(): Promise<void> {
|
|
|
110
110
|
log('');
|
|
111
111
|
log(`${c.bold} Next steps:${c.reset}`);
|
|
112
112
|
dim(`cd ${name}`);
|
|
113
|
-
|
|
113
|
+
// Every template that ships baseline migrations builds its schema from them
|
|
114
|
+
// (synchronize is off), so the first migrate is a required step, not a tip.
|
|
115
|
+
if (template === 'api' || template === 'flow' || template === 'react' || template === 'vue') {
|
|
114
116
|
if (db !== 'sqlite') {
|
|
115
117
|
dim(`# Set DATABASE_URL in .env`);
|
|
116
118
|
}
|
package/src/scaffold.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type Template = 'minimal' | 'api' | 'admin' | 'flow' | 'react' | 'vue';
|
|
|
13
13
|
// "^1.1.0" found for specifier "zerotal"` — the first thing anyone trying the
|
|
14
14
|
// framework saw. `scaffold.test.ts` now asserts the two agree, so CI fails rather
|
|
15
15
|
// than the user's install.
|
|
16
|
-
export const ZT_VERSION = '^1.0
|
|
16
|
+
export const ZT_VERSION = '^1.1.0';
|
|
17
17
|
|
|
18
18
|
export interface ScaffoldOptions {
|
|
19
19
|
name: string;
|
|
@@ -73,12 +73,18 @@ export async function scaffold(opts: ScaffoldOptions): Promise<void> {
|
|
|
73
73
|
const src = join(templateDir, rel);
|
|
74
74
|
// Rename underscore-prefixed dotfiles — npm and Bun glob both skip real dotfiles
|
|
75
75
|
// inside published/scanned directories. _gitignore → .gitignore, etc.
|
|
76
|
-
//
|
|
77
|
-
//
|
|
76
|
+
//
|
|
77
|
+
// Test files carry a trailing `.tmpl` that is stripped here, so Bun does not
|
|
78
|
+
// discover them while this package is developed inside the monorepo — where their
|
|
79
|
+
// `zerotal/testing` imports cannot resolve, because they are written for the app
|
|
80
|
+
// being scaffolded rather than for this workspace. The previous spelling was
|
|
81
|
+
// `_test.ts`, which does not work: Bun's matcher globs `_test.ts` as readily as
|
|
82
|
+
// `.test.ts` (and `.spec.ts` / `_spec.ts`), so those files were collected and failed
|
|
83
|
+
// on every root `bun test`. A trailing suffix is matched by none of them.
|
|
78
84
|
const dest = join(opts.target, rel
|
|
79
85
|
.replace('_gitignore', '.gitignore')
|
|
80
86
|
.replace('_env.example', '.env.example')
|
|
81
|
-
.replace('
|
|
87
|
+
.replace(/\.tmpl$/, ''),
|
|
82
88
|
);
|
|
83
89
|
|
|
84
90
|
await mkdir(dirname(dest), { recursive: true });
|
|
@@ -11,11 +11,11 @@ export class Product extends BaseModel {
|
|
|
11
11
|
|
|
12
12
|
@column() name!: string;
|
|
13
13
|
@column() sku!: string;
|
|
14
|
-
@column({ nullable: true }) description?: string;
|
|
14
|
+
@column({ nullable: true }) description?: string | undefined;
|
|
15
15
|
/** Price in minor units, so money never rides on a float. */
|
|
16
16
|
@column("integer") price!: number;
|
|
17
|
-
@column({ cast: "integer", nullable: true }) stock?: number;
|
|
17
|
+
@column({ cast: "integer", nullable: true }) stock?: number | undefined;
|
|
18
18
|
/** draft | active | discontinued — drives the filter tabs and a select column. */
|
|
19
|
-
@column({ nullable: true }) status?: string;
|
|
20
|
-
@column({ cast: "boolean", nullable: true }) featured?: boolean;
|
|
19
|
+
@column({ nullable: true }) status?: string | undefined;
|
|
20
|
+
@column({ cast: "boolean", nullable: true }) featured?: boolean | undefined;
|
|
21
21
|
}
|
|
@@ -9,6 +9,6 @@ export class Setting extends BaseModel {
|
|
|
9
9
|
static override fillable = ["siteName", "supportEmail", "ordersOpen"];
|
|
10
10
|
|
|
11
11
|
@column() siteName!: string;
|
|
12
|
-
@column({ nullable: true }) supportEmail?: string;
|
|
13
|
-
@column({ cast: "boolean", nullable: true }) ordersOpen?: boolean;
|
|
12
|
+
@column({ nullable: true }) supportEmail?: string | undefined;
|
|
13
|
+
@column({ cast: "boolean", nullable: true }) ordersOpen?: boolean | undefined;
|
|
14
14
|
}
|
|
@@ -14,7 +14,7 @@ export class User extends BaseModelWith(Authenticatable) {
|
|
|
14
14
|
@column() email!: string;
|
|
15
15
|
@column() password!: string;
|
|
16
16
|
/** Assigned roles — drives a multi-select field and a badge column. */
|
|
17
|
-
@column({ cast: "json", nullable: true }) roles?: string[];
|
|
17
|
+
@column({ cast: "json", nullable: true }) roles?: string[] | undefined;
|
|
18
18
|
/** Profile picture, shown as a circular image entry on the view page. */
|
|
19
|
-
@column({ nullable: true }) avatarUrl?: string;
|
|
19
|
+
@column({ nullable: true }) avatarUrl?: string | undefined;
|
|
20
20
|
}
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import { LogProvider } from "zerotal/logger";
|
|
2
2
|
import { DatabaseProvider } from "zerotal/orm";
|
|
3
|
+
import { StorageProvider } from "zerotal/storage";
|
|
3
4
|
import { CacheProvider } from "zerotal/cache";
|
|
4
5
|
import { SessionProvider } from "zerotal/session";
|
|
5
6
|
import { AuthProvider } from "zerotal/auth";
|
|
6
7
|
import { FlowProvider } from "@zerotal/flow";
|
|
7
8
|
import { AdminProvider } from "@zerotal/admin";
|
|
9
|
+
import { DevtoolsProvider } from "@zerotal/devtools";
|
|
8
10
|
|
|
9
11
|
// Order matters: the database backs every resource, the cache holds the panel's
|
|
10
12
|
// navigation-badge counts, and the session underpins auth. FlowProvider is
|
|
11
13
|
// listed explicitly (before AdminProvider, which also `dependsOn` it) so its CLI
|
|
12
14
|
// tooling boots in `bun zt`, where the admin panel itself does not run.
|
|
15
|
+
//
|
|
16
|
+
// `DevtoolsProvider` is last and needs no configuration. It activates only when
|
|
17
|
+
// APP_ENV is one of development/dev/local/test/testing — it fails *closed*, so
|
|
18
|
+
// an unset or `staging` APP_ENV leaves it inert rather than exposing the trace
|
|
19
|
+
// inspector. It also injects its own in-page panel, so nothing has to be added
|
|
20
|
+
// to the panel's assets. Press Alt+D (Cmd+D on Mac) to open it.
|
|
21
|
+
//
|
|
22
|
+
// It is a real dependency rather than a devDependency on purpose: this import
|
|
23
|
+
// runs in every environment, so a `--production` install that dropped the
|
|
24
|
+
// package would fail at boot instead of simply skipping the panel.
|
|
13
25
|
const providers = [
|
|
14
26
|
LogProvider,
|
|
15
27
|
DatabaseProvider,
|
|
28
|
+
StorageProvider,
|
|
16
29
|
CacheProvider,
|
|
17
30
|
SessionProvider,
|
|
18
31
|
AuthProvider,
|
|
19
32
|
FlowProvider,
|
|
20
33
|
AdminProvider,
|
|
34
|
+
DevtoolsProvider,
|
|
21
35
|
];
|
|
22
36
|
|
|
23
37
|
export default providers;
|
|
@@ -12,5 +12,5 @@ export class User extends BaseModel {
|
|
|
12
12
|
@column({ type: 'string' }) name!: string;
|
|
13
13
|
@column({ type: 'string' }) email!: string;
|
|
14
14
|
@column({ type: 'string' }) password!: string;
|
|
15
|
-
@column({ type: 'string', nullable: true, default: 'user' }) role?: string;
|
|
15
|
+
@column({ type: 'string', nullable: true, default: 'user' }) role?: string | undefined;
|
|
16
16
|
}
|
|
@@ -5,7 +5,20 @@ import { AuthProvider } from 'zerotal/auth';
|
|
|
5
5
|
import { NotificationProvider } from '@zerotal/notifications';
|
|
6
6
|
import { QueueProvider } from 'zerotal/queue';
|
|
7
7
|
import { LogProvider } from 'zerotal/logger';
|
|
8
|
+
import { DevtoolsProvider } from '@zerotal/devtools';
|
|
8
9
|
|
|
10
|
+
// `DevtoolsProvider` needs no configuration. It activates only when APP_ENV is
|
|
11
|
+
// one of development/dev/local/test/testing — it fails *closed*, so an unset or
|
|
12
|
+
// `staging` APP_ENV leaves it inert rather than exposing the trace inspector.
|
|
13
|
+
//
|
|
14
|
+
// This API answers with JSON, so there is no HTML for the floating panel to
|
|
15
|
+
// attach itself to. Open `http://localhost:3000/__zerotal/devtools` instead —
|
|
16
|
+
// the dashboard is served as its own page, and every request this API handles
|
|
17
|
+
// still records its SQL, N+1 warnings, jobs, mail and logs there.
|
|
18
|
+
//
|
|
19
|
+
// It is a real dependency rather than a devDependency on purpose: this import
|
|
20
|
+
// runs in every environment, so a `--production` install that dropped the
|
|
21
|
+
// package would fail at boot instead of simply skipping the panel.
|
|
9
22
|
export default Application.create({
|
|
10
23
|
providers: [
|
|
11
24
|
DatabaseProvider,
|
|
@@ -14,6 +27,7 @@ export default Application.create({
|
|
|
14
27
|
NotificationProvider,
|
|
15
28
|
QueueProvider,
|
|
16
29
|
LogProvider,
|
|
30
|
+
DevtoolsProvider,
|
|
17
31
|
],
|
|
18
32
|
})
|
|
19
33
|
.routing({ web: `${import.meta.dir}/../routes/index.ts` });
|
|
@@ -48,7 +48,15 @@ export class RegisterPage extends Component {
|
|
|
48
48
|
user.role = "user";
|
|
49
49
|
await user.save();
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// Check the result. Redirecting to a guarded page on a failed attempt sends
|
|
52
|
+
// the visitor to /profile, where AuthMiddleware turns them straight back to
|
|
53
|
+
// /login — so the account is created and they are bounced to a sign-in form
|
|
54
|
+
// with no error shown anywhere. Report it instead of redirecting into it.
|
|
55
|
+
if (!(await Auth.attempt({ email: this.email, password: this.password }))) {
|
|
56
|
+
this.error = "Your account was created, but signing you in failed. Please sign in.";
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
this.redirect("/profile").withSuccess("Welcome aboard.");
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseModelWith, column, table } from "zerotal/orm";
|
|
2
|
+
import { Authenticatable } from "zerotal/auth";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `BaseModelWith(Authenticatable)`, not a plain `BaseModel`.
|
|
6
|
+
*
|
|
7
|
+
* The mixin is what brands the class so `authUserModel()` can find it. Without
|
|
8
|
+
* the brand nothing errors and nothing warns — the model saves, queries and
|
|
9
|
+
* hashes exactly as before — but `Auth.attempt()` resolves *no* user model at
|
|
10
|
+
* all and therefore returns `false` for every correct password. Registration
|
|
11
|
+
* appears to work while login silently never succeeds.
|
|
12
|
+
*
|
|
13
|
+
* It also supplies `getAuthId()` / `getAuthPassword()`, which the auth flow
|
|
14
|
+
* reads, and registers the `rememberToken` column that "remember me" needs.
|
|
15
|
+
*/
|
|
3
16
|
@table("users")
|
|
4
|
-
export class User extends
|
|
17
|
+
export class User extends BaseModelWith(Authenticatable) {
|
|
5
18
|
// Models guard every attribute by default. Only these may be mass-assigned
|
|
6
19
|
// from a request body — `role` is deliberately absent, so no amount of extra
|
|
7
20
|
// fields in a form post can promote the account that submitted it.
|
|
@@ -13,5 +26,5 @@ export class User extends BaseModel {
|
|
|
13
26
|
@column({ type: "string" }) name!: string;
|
|
14
27
|
@column({ type: "string" }) email!: string;
|
|
15
28
|
@column({ type: "string" }) password!: string;
|
|
16
|
-
@column({ type: "string", nullable: true, default: "user" }) role?: string;
|
|
29
|
+
@column({ type: "string", nullable: true, default: "user" }) role?: string | undefined;
|
|
17
30
|
}
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { LogProvider } from "zerotal/logger";
|
|
2
2
|
import { DatabaseProvider } from "zerotal/orm";
|
|
3
|
+
import { StorageProvider } from "zerotal/storage";
|
|
3
4
|
import { SessionProvider } from "zerotal/session";
|
|
4
5
|
import { AuthProvider } from "zerotal/auth";
|
|
5
6
|
import { FlowProvider } from "@zerotal/flow";
|
|
7
|
+
import { DevtoolsProvider } from "@zerotal/devtools";
|
|
6
8
|
|
|
7
9
|
// Order matters: the database backs the user model, and the session underpins
|
|
8
10
|
// auth — AuthProvider needs both already registered when it boots.
|
|
9
|
-
|
|
11
|
+
//
|
|
12
|
+
// `DevtoolsProvider` is last and needs no configuration. It activates only when
|
|
13
|
+
// APP_ENV is one of development/dev/local/test/testing — it fails *closed*, so
|
|
14
|
+
// an unset or `staging` APP_ENV leaves it inert rather than exposing the trace
|
|
15
|
+
// inspector. It also injects its own in-page panel, so nothing has to be added
|
|
16
|
+
// to the frontend bundle. Press Alt+D (Cmd+D on Mac) to open it.
|
|
17
|
+
//
|
|
18
|
+
// It is a real dependency rather than a devDependency on purpose: this import
|
|
19
|
+
// runs in every environment, so a `--production` install that dropped the
|
|
20
|
+
// package would fail at boot instead of simply skipping the panel.
|
|
21
|
+
const providers = [
|
|
22
|
+
LogProvider,
|
|
23
|
+
DatabaseProvider,
|
|
24
|
+
StorageProvider,
|
|
25
|
+
SessionProvider,
|
|
26
|
+
AuthProvider,
|
|
27
|
+
FlowProvider,
|
|
28
|
+
DevtoolsProvider,
|
|
29
|
+
];
|
|
10
30
|
|
|
11
31
|
export default providers;
|
|
@@ -11,8 +11,10 @@ export default DatabaseConfig({
|
|
|
11
11
|
// against your development database and leave its rows behind.
|
|
12
12
|
url: env("ZT_DB_URL", env("DATABASE_URL", "./database/db.sqlite")),
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// Off deliberately. This template ships baseline migrations in
|
|
15
|
+
// database/migrations, and boot-time schema sync would create the same tables
|
|
16
|
+
// from the models first — so the very first `bun zt migrate` would fail with
|
|
17
|
+
// "table users already exists". Migrations are the single source of truth, in
|
|
18
|
+
// development exactly as in production; run `bun zt migrate` after scaffolding.
|
|
19
|
+
synchronize: false,
|
|
18
20
|
});
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* does rather than that it exists.
|
|
9
9
|
*/
|
|
10
10
|
import { beforeAll, afterAll, describe, test, expect } from 'bun:test';
|
|
11
|
-
import { createTestApp, type TestApp } from 'zerotal/testing';
|
|
11
|
+
import { createTestApp, migrateDatabase, type TestApp } from 'zerotal/testing';
|
|
12
12
|
import { FlowTest } from '@zerotal/flow/testing';
|
|
13
|
+
import { Auth, Hash, isAuthenticatable } from 'zerotal/auth';
|
|
13
14
|
import { ContactPage } from '../app/flow/pages/contact.tsx';
|
|
15
|
+
import { User } from '../app/models/User.ts';
|
|
14
16
|
|
|
15
17
|
let app: TestApp;
|
|
16
18
|
|
|
@@ -22,6 +24,12 @@ beforeAll(async () => {
|
|
|
22
24
|
// Each run gets its own throwaway schema rather than the dev database.
|
|
23
25
|
Bun.env.ZT_DB_URL ??= ':memory:';
|
|
24
26
|
app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
|
|
27
|
+
// Build the schema from the project's own migrations. config/database.ts keeps
|
|
28
|
+
// synchronize off — migrations are the single source of truth — so without this
|
|
29
|
+
// the :memory: database above has no tables and every test fails on 'no such
|
|
30
|
+
// table'. Running the real migrations also means the schema under test is the
|
|
31
|
+
// schema that ships, rather than a second definition that drifts from it.
|
|
32
|
+
await migrateDatabase();
|
|
25
33
|
});
|
|
26
34
|
|
|
27
35
|
afterAll(() => app.close());
|
|
@@ -73,6 +81,46 @@ describe('auth', () => {
|
|
|
73
81
|
|
|
74
82
|
expect(res.status).toBe(302);
|
|
75
83
|
});
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* Credentials must actually verify. Asserting that /login and /register
|
|
87
|
+
* *render* proves nothing about whether anyone can get in, and that gap let a
|
|
88
|
+
* User model ship that the auth layer could not resolve at all: every correct
|
|
89
|
+
* password was rejected, and the only symptom was being returned to the
|
|
90
|
+
* sign-in form. Nothing threw, and every other test still passed.
|
|
91
|
+
*/
|
|
92
|
+
/*
|
|
93
|
+
* `validate` rather than `attempt`: it runs the same user lookup and password
|
|
94
|
+
* check, but stops short of `Auth.login`, which needs a live request context
|
|
95
|
+
* that a plain unit test has no reason to build. The bug this guards against
|
|
96
|
+
* lives entirely in the lookup half.
|
|
97
|
+
*/
|
|
98
|
+
test('a registered user can actually sign in', async () => {
|
|
99
|
+
const user = new User();
|
|
100
|
+
user.name = 'Test Person';
|
|
101
|
+
user.email = 'signin-check@example.com';
|
|
102
|
+
user.password = await Hash.make('correct-horse-battery');
|
|
103
|
+
user.role = 'user';
|
|
104
|
+
await user.save();
|
|
105
|
+
|
|
106
|
+
expect(await Auth.validate({
|
|
107
|
+
email: 'signin-check@example.com',
|
|
108
|
+
password: 'correct-horse-battery',
|
|
109
|
+
})).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the User model is resolvable by the auth layer', () => {
|
|
113
|
+
// The root cause, asserted directly: `Auth.attempt` finds the user model
|
|
114
|
+
// through this brand, so a plain `BaseModel` makes every login fail.
|
|
115
|
+
expect(isAuthenticatable(User)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a wrong password is still refused', async () => {
|
|
119
|
+
expect(await Auth.validate({
|
|
120
|
+
email: 'signin-check@example.com',
|
|
121
|
+
password: 'not-the-password',
|
|
122
|
+
})).toBe(false);
|
|
123
|
+
});
|
|
76
124
|
});
|
|
77
125
|
|
|
78
126
|
/**
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Application, basePath } from 'zerotal';
|
|
2
2
|
import { LogProvider } from 'zerotal/logger';
|
|
3
|
+
import { DevtoolsProvider } from '@zerotal/devtools';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// `DevtoolsProvider` needs no configuration. It activates only when APP_ENV is
|
|
6
|
+
// one of development/dev/local/test/testing — it fails *closed*, so an unset or
|
|
7
|
+
// `staging` APP_ENV leaves it inert rather than exposing the trace inspector. It
|
|
8
|
+
// injects its own in-page panel, so nothing has to be added to the frontend
|
|
9
|
+
// bundle. Press Alt+D (Cmd+D on Mac) to open it.
|
|
10
|
+
//
|
|
11
|
+
// It is a real dependency rather than a devDependency on purpose: this import
|
|
12
|
+
// runs in every environment, so a `--production` install that dropped the
|
|
13
|
+
// package would fail at boot instead of simply skipping the panel.
|
|
14
|
+
const app = Application.create({ providers: [LogProvider, DevtoolsProvider] })
|
|
5
15
|
.routing({
|
|
6
16
|
web: basePath("routes/index.ts"),
|
|
7
17
|
});
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseModelWith, column, table } from "zerotal/orm";
|
|
2
|
+
import { Authenticatable } from "zerotal/auth";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `BaseModelWith(Authenticatable)`, not a plain `BaseModel`.
|
|
6
|
+
*
|
|
7
|
+
* The mixin is what brands the class so `authUserModel()` can find it. Without
|
|
8
|
+
* the brand nothing errors and nothing warns — the model saves, queries and
|
|
9
|
+
* hashes exactly as before — but `Auth.attempt()` resolves *no* user model at
|
|
10
|
+
* all and therefore returns `false` for every correct password, so no one can
|
|
11
|
+
* ever sign in.
|
|
12
|
+
*
|
|
13
|
+
* It also supplies `getAuthId()` / `getAuthPassword()`, which the auth flow
|
|
14
|
+
* reads, and registers the `rememberToken` column that "remember me" needs.
|
|
15
|
+
*/
|
|
3
16
|
@table("users")
|
|
4
|
-
export class User extends
|
|
17
|
+
export class User extends BaseModelWith(Authenticatable) {
|
|
5
18
|
// Models guard every attribute by default. Only these may be mass-assigned
|
|
6
19
|
// from a request body — `role` is deliberately absent, so no amount of extra
|
|
7
20
|
// fields in a form post can promote the account that submitted it.
|
|
@@ -13,5 +26,5 @@ export class User extends BaseModel {
|
|
|
13
26
|
@column({ type: "string" }) name!: string;
|
|
14
27
|
@column({ type: "string" }) email!: string;
|
|
15
28
|
@column({ type: "string" }) password!: string;
|
|
16
|
-
@column({ type: "string", nullable: true, default: "user" }) role?: string;
|
|
29
|
+
@column({ type: "string", nullable: true, default: "user" }) role?: string | undefined;
|
|
17
30
|
}
|
|
@@ -33,7 +33,16 @@ export async function POST(http: HttpContext): Promise<void> {
|
|
|
33
33
|
user.role = "user";
|
|
34
34
|
await user.save();
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Check the result. Redirecting to a guarded page on a failed attempt sends
|
|
37
|
+
// the visitor to /profile, where the auth guard turns them straight back to
|
|
38
|
+
// /login — so the account is created and they land on a sign-in form with no
|
|
39
|
+
// error shown anywhere. Say what happened instead of redirecting into it.
|
|
40
|
+
if (!(await Auth.attempt({ email, password }))) {
|
|
41
|
+
http.flash("error", "Your account was created, but signing you in failed. Please sign in.");
|
|
42
|
+
http.redirect("/login", 303);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
http.flash("success", "Welcome aboard.");
|
|
38
47
|
http.redirect("/profile", 303);
|
|
39
48
|
}
|
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import { LogProvider } from "zerotal/logger";
|
|
2
2
|
import { DatabaseProvider } from "zerotal/orm";
|
|
3
|
+
import { StorageProvider } from "zerotal/storage";
|
|
3
4
|
import { SessionProvider } from "zerotal/session";
|
|
4
5
|
import { AuthProvider } from "zerotal/auth";
|
|
5
6
|
import { InertiaProvider } from "@zerotal/inertia";
|
|
7
|
+
import { DevtoolsProvider } from "@zerotal/devtools";
|
|
6
8
|
|
|
7
9
|
// Order matters: the database backs the user model and the session underpins
|
|
8
10
|
// auth, so both are registered before AuthProvider boots. AuthProvider installs
|
|
9
11
|
// the middleware that puts the signed-in user on the request — which is what
|
|
10
12
|
// makes `auth.user` appear in every Inertia page's props.
|
|
11
|
-
|
|
13
|
+
//
|
|
14
|
+
// `DevtoolsProvider` is last and needs no configuration. It activates only when
|
|
15
|
+
// APP_ENV is one of development/dev/local/test/testing — it fails *closed*, so
|
|
16
|
+
// an unset or `staging` APP_ENV leaves it inert rather than exposing the trace
|
|
17
|
+
// inspector. It also injects its own in-page panel, so nothing has to be added
|
|
18
|
+
// to the React bundle. Press Alt+D (Cmd+D on Mac) to open it.
|
|
19
|
+
//
|
|
20
|
+
// It is a real dependency rather than a devDependency on purpose: this import
|
|
21
|
+
// runs in every environment, so a `--production` install that dropped the
|
|
22
|
+
// package would fail at boot instead of simply skipping the panel.
|
|
23
|
+
const providers = [
|
|
24
|
+
LogProvider,
|
|
25
|
+
DatabaseProvider,
|
|
26
|
+
StorageProvider,
|
|
27
|
+
SessionProvider,
|
|
28
|
+
AuthProvider,
|
|
29
|
+
InertiaProvider,
|
|
30
|
+
DevtoolsProvider,
|
|
31
|
+
];
|
|
12
32
|
|
|
13
33
|
export default providers;
|
|
@@ -11,8 +11,10 @@ export default DatabaseConfig({
|
|
|
11
11
|
// against your development database and leave its rows behind.
|
|
12
12
|
url: env("ZT_DB_URL", env("DATABASE_URL", "./database/db.sqlite")),
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// Off deliberately. This template ships baseline migrations in
|
|
15
|
+
// database/migrations, and boot-time schema sync would create the same tables
|
|
16
|
+
// from the models first — so the very first `bun zt migrate` would fail with
|
|
17
|
+
// "table users already exists". Migrations are the single source of truth, in
|
|
18
|
+
// development exactly as in production; run `bun zt migrate` after scaffolding.
|
|
19
|
+
synchronize: false,
|
|
18
20
|
});
|
|
@@ -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, migrateDatabase, type TestApp } from 'zerotal/testing';
|
|
11
11
|
|
|
12
12
|
let app: TestApp;
|
|
13
13
|
|
|
@@ -18,6 +18,12 @@ beforeAll(async () => {
|
|
|
18
18
|
// Each run gets its own throwaway schema rather than the dev database.
|
|
19
19
|
Bun.env.ZT_DB_URL ??= ':memory:';
|
|
20
20
|
app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
|
|
21
|
+
// Build the schema from the project's own migrations. config/database.ts keeps
|
|
22
|
+
// synchronize off — migrations are the single source of truth — so without this
|
|
23
|
+
// the :memory: database above has no tables and every test fails on 'no such
|
|
24
|
+
// table'. Running the real migrations also means the schema under test is the
|
|
25
|
+
// schema that ships, rather than a second definition that drifts from it.
|
|
26
|
+
await migrateDatabase();
|
|
21
27
|
});
|
|
22
28
|
|
|
23
29
|
afterAll(() => app.close());
|
|
@@ -13,5 +13,5 @@ export class User extends BaseModel {
|
|
|
13
13
|
@column({ type: "string" }) name!: string;
|
|
14
14
|
@column({ type: "string" }) email!: string;
|
|
15
15
|
@column({ type: "string" }) password!: string;
|
|
16
|
-
@column({ type: "string", nullable: true, default: "user" }) role?: string;
|
|
16
|
+
@column({ type: "string", nullable: true, default: "user" }) role?: string | undefined;
|
|
17
17
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LogProvider } from "zerotal/logger";
|
|
2
2
|
import { DatabaseProvider } from "zerotal/orm";
|
|
3
|
+
import { StorageProvider } from "zerotal/storage";
|
|
3
4
|
import { SessionProvider } from "zerotal/session";
|
|
4
5
|
import { AuthProvider } from "zerotal/auth";
|
|
5
6
|
import { InertiaProvider } from "@zerotal/inertia";
|
|
@@ -11,8 +11,10 @@ export default DatabaseConfig({
|
|
|
11
11
|
// against your development database and leave its rows behind.
|
|
12
12
|
url: env("ZT_DB_URL", env("DATABASE_URL", "./database/db.sqlite")),
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// Off deliberately. This template ships baseline migrations in
|
|
15
|
+
// database/migrations, and boot-time schema sync would create the same tables
|
|
16
|
+
// from the models first — so the very first `bun zt migrate` would fail with
|
|
17
|
+
// "table users already exists". Migrations are the single source of truth, in
|
|
18
|
+
// development exactly as in production; run `bun zt migrate` after scaffolding.
|
|
19
|
+
synchronize: false,
|
|
18
20
|
});
|
|
@@ -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, migrateDatabase, type TestApp } from 'zerotal/testing';
|
|
11
11
|
|
|
12
12
|
let app: TestApp;
|
|
13
13
|
|
|
@@ -18,6 +18,12 @@ beforeAll(async () => {
|
|
|
18
18
|
// Each run gets its own throwaway schema rather than the dev database.
|
|
19
19
|
Bun.env.ZT_DB_URL ??= ':memory:';
|
|
20
20
|
app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
|
|
21
|
+
// Build the schema from the project's own migrations. config/database.ts keeps
|
|
22
|
+
// synchronize off — migrations are the single source of truth — so without this
|
|
23
|
+
// the :memory: database above has no tables and every test fails on 'no such
|
|
24
|
+
// table'. Running the real migrations also means the schema under test is the
|
|
25
|
+
// schema that ships, rather than a second definition that drifts from it.
|
|
26
|
+
await migrateDatabase();
|
|
21
27
|
});
|
|
22
28
|
|
|
23
29
|
afterAll(() => app.close());
|
|
File without changes
|
|
File without changes
|
|
File without changes
|