create-zerotal 1.0.1 → 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.
- package/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/scaffold.ts +1 -1
- package/templates/admin/README.md +41 -0
- package/templates/admin/_env.example +17 -0
- package/templates/admin/app/admin/index.ts +6 -0
- package/templates/admin/app/auth/passwords.ts +79 -0
- package/templates/admin/config/database.ts +4 -3
- package/templates/admin/database/migrations/0001_create_password_reset_tokens_table.ts +26 -0
- package/templates/admin/package.json +9 -1
- package/templates/admin/tsconfig.json +7 -2
- package/templates/api/README.md +39 -0
- package/templates/api/_env.example +13 -0
- package/templates/api/app/controllers/AuthController.ts +11 -8
- package/templates/api/app/controllers/WelcomeController.ts +1 -1
- package/templates/api/app/middleware/RequireAuth.ts +1 -1
- package/templates/api/app/models/User.ts +2 -2
- package/templates/api/bootstrap/app.ts +6 -6
- package/templates/api/config/app.ts +1 -1
- package/templates/api/config/database.ts +12 -3
- package/templates/api/config/notifications.ts +1 -1
- package/templates/api/config/queue.ts +1 -1
- package/templates/api/config/session.ts +2 -2
- package/templates/api/database/migrations/0001_create_users_table.ts +8 -8
- package/templates/api/package.json +9 -1
- package/templates/api/routes/index.ts +23 -8
- package/templates/api/tests/auth.test.ts +6 -3
- package/templates/api/tests/helpers.ts +6 -6
- package/templates/api/tsconfig.json +7 -3
- package/templates/flow/README.md +39 -0
- package/templates/flow/_env.example +17 -0
- package/templates/flow/app/auth/passwords.ts +79 -0
- package/templates/flow/app/flow/layouts/app.tsx +10 -1
- package/templates/flow/app/flow/pages/forgot-password.tsx +69 -0
- package/templates/flow/app/flow/pages/login.tsx +88 -0
- package/templates/flow/app/flow/pages/profile.tsx +194 -0
- package/templates/flow/app/flow/pages/register.tsx +120 -0
- package/templates/flow/app/flow/pages/reset-password.tsx +103 -0
- package/templates/flow/app/flow/ui.ts +27 -0
- package/templates/flow/app/models/User.ts +17 -0
- package/templates/flow/bootstrap/app.ts +8 -0
- package/templates/flow/bootstrap/providers.ts +6 -1
- package/templates/flow/config/auth.ts +7 -0
- package/templates/flow/config/database.ts +18 -0
- package/templates/flow/config/session.ts +14 -0
- package/templates/flow/database/migrations/0001_create_users_table.ts +18 -0
- package/templates/flow/database/migrations/0002_create_password_reset_tokens_table.ts +23 -0
- package/templates/flow/package.json +10 -4
- package/templates/flow/tests/smoke.test.ts +21 -1
- package/templates/flow/tsconfig.json +10 -3
- package/templates/minimal/README.md +39 -0
- package/templates/minimal/_env.example +9 -0
- package/templates/minimal/bootstrap/app.ts +2 -2
- package/templates/minimal/package.json +10 -5
- package/templates/minimal/tests/smoke.test.ts +1 -1
- package/templates/minimal/tsconfig.json +13 -4
- package/templates/react/README.md +40 -0
- package/templates/react/_env.example +18 -0
- package/templates/react/app/auth/passwords.ts +79 -0
- package/templates/react/app/models/User.ts +17 -0
- package/templates/react/app/routes/forgot-password.ts +24 -0
- package/templates/react/app/routes/login.ts +32 -0
- package/templates/react/app/routes/logout.ts +12 -0
- package/templates/react/app/routes/profile/password.ts +30 -0
- package/templates/react/app/routes/profile.ts +39 -0
- package/templates/react/app/routes/register.ts +39 -0
- package/templates/react/app/routes/reset-password.ts +37 -0
- package/templates/react/bootstrap/app.ts +8 -0
- package/templates/react/bootstrap/providers.ts +7 -4
- package/templates/react/config/auth.ts +7 -0
- package/templates/react/config/database.ts +18 -0
- package/templates/react/database/migrations/0001_create_users_table.ts +18 -0
- package/templates/react/database/migrations/0002_create_password_reset_tokens_table.ts +23 -0
- package/templates/react/package.json +8 -2
- package/templates/react/resources/js/Layouts/AppLayout.tsx +12 -2
- package/templates/react/resources/js/pages/forgot-password.tsx +65 -0
- package/templates/react/resources/js/pages/login.tsx +92 -0
- package/templates/react/resources/js/pages/profile.tsx +147 -0
- package/templates/react/resources/js/pages/register.tsx +101 -0
- package/templates/react/resources/js/pages/reset-password.tsx +80 -0
- package/templates/react/resources/js/pages.generated.ts +5 -0
- package/templates/react/tests/smoke.test.ts +58 -2
- package/templates/react/tsconfig.json +10 -3
- package/templates/vue/README.md +40 -0
- package/templates/vue/_env.example +18 -0
- package/templates/vue/app/auth/passwords.ts +79 -0
- package/templates/vue/app/exceptions/Handler.ts +55 -0
- package/templates/vue/app/models/User.ts +17 -0
- package/templates/vue/app/routes/forgot-password.ts +24 -0
- package/templates/vue/app/routes/login.ts +32 -0
- package/templates/vue/app/routes/logout.ts +12 -0
- package/templates/vue/app/routes/profile/password.ts +30 -0
- package/templates/vue/app/routes/profile.ts +39 -0
- package/templates/vue/app/routes/register.ts +39 -0
- package/templates/vue/app/routes/reset-password.ts +37 -0
- package/templates/vue/bootstrap/app.ts +15 -3
- package/templates/vue/bootstrap/providers.ts +8 -1
- package/templates/vue/config/auth.ts +7 -0
- package/templates/vue/config/database.ts +18 -0
- package/templates/vue/config/session.ts +11 -0
- package/templates/vue/database/migrations/0001_create_users_table.ts +18 -0
- package/templates/vue/database/migrations/0002_create_password_reset_tokens_table.ts +23 -0
- package/templates/vue/package.json +8 -2
- package/templates/vue/public/zt.svg +17 -0
- package/templates/vue/resources/app.html +27 -1
- package/templates/vue/resources/css/app.css +205 -0
- package/templates/vue/resources/js/Components/Button.vue +49 -0
- package/templates/vue/resources/js/Components/Card.vue +23 -0
- package/templates/vue/resources/js/Components/FlashToasts.vue +81 -0
- package/templates/vue/resources/js/Components/Icon.vue +40 -0
- package/templates/vue/resources/js/Components/TextField.vue +66 -0
- package/templates/vue/resources/js/Components/ThemeToggle.vue +46 -0
- package/templates/vue/resources/js/Layouts/AppLayout.vue +140 -25
- package/templates/vue/resources/js/lib/cn.ts +13 -0
- package/templates/vue/resources/js/lib/site.ts +11 -0
- package/templates/vue/resources/js/pages/error.vue +47 -0
- package/templates/vue/resources/js/pages/forgot-password.vue +47 -0
- package/templates/vue/resources/js/pages/login.vue +67 -0
- package/templates/vue/resources/js/pages/profile.vue +119 -0
- package/templates/vue/resources/js/pages/register.vue +77 -0
- package/templates/vue/resources/js/pages/reset-password.vue +58 -0
- package/templates/vue/resources/js/pages.generated.ts +10 -4
- package/templates/vue/resources/js/types.ts +17 -0
- package/templates/vue/tests/smoke.test.ts +50 -1
- package/templates/vue/tsconfig.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.2] — 2026-08-06
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Templates could not be type-checked and, for two database choices, could not
|
|
16
|
+
boot.** `api` scaffolded with PostgreSQL or MySQL wrote a matching
|
|
17
|
+
`DATABASE_URL` but left the driver at its `sqlite` default — the exact pairing
|
|
18
|
+
boot validation rejects. No template declared `@types/bun`, so `tsc` failed in
|
|
19
|
+
all six; none had a `typecheck` script to reveal it.
|
|
20
|
+
- `flow` and `minimal` listed Tailwind as a dev dependency although `zt serve`
|
|
21
|
+
builds their assets at boot, so `bun install --production` broke them.
|
|
22
|
+
- Templates imported packages they did not declare, resolving only via hoisting.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- A README, an `engines` floor, a `typecheck` script and a documented
|
|
27
|
+
`.env.example` in every template.
|
|
28
|
+
|
|
11
29
|
## [1.0.1] — 2026-08-06
|
|
12
30
|
|
|
13
31
|
### Fixed
|
package/package.json
CHANGED
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.0.2';
|
|
17
17
|
|
|
18
18
|
export interface ScaffoldOptions {
|
|
19
19
|
name: string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
An admin panel: resources, authentication, dashboard widgets and seeded demo data.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Built with [Zerotal](https://zerotal.dev) — a full-stack TypeScript framework for Bun.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- [Bun](https://bun.sh) 1.3.14 or newer. Node.js is not supported: Zerotal uses
|
|
11
|
+
Bun-native APIs throughout.
|
|
12
|
+
|
|
13
|
+
## Getting started
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun install
|
|
17
|
+
bun run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The dev server prints a local URL and a network one, so you can open the app on
|
|
21
|
+
another device on the same Wi-Fi.
|
|
22
|
+
|
|
23
|
+
## Scripts
|
|
24
|
+
|
|
25
|
+
| Command | What it does |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `bun run dev` | Start the dev server with hot reload |
|
|
28
|
+
| `bun run start` | Start the server without dev tooling |
|
|
29
|
+
| `bun run test` | Run the test suite |
|
|
30
|
+
| `bun run typecheck` | Type-check without emitting |
|
|
31
|
+
| `bun run seed` | Re-seed the demo data |
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Environment variables live in `.env`, documented in `.env.example`. `APP_KEY`
|
|
36
|
+
was generated for this project when it was scaffolded — keep it out of version
|
|
37
|
+
control, and use a different one per environment.
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
Full documentation is at [zerotal.dev/docs](https://zerotal.dev/docs).
|
|
@@ -1,2 +1,19 @@
|
|
|
1
|
+
# The runtime environment. "production" turns on the framework's stricter
|
|
2
|
+
# config checks and turns off verbose error output.
|
|
1
3
|
APP_ENV=development
|
|
4
|
+
|
|
5
|
+
# Signs sessions, cookies and encrypted values. Generated per project — never
|
|
6
|
+
# reuse one between environments, and never commit a real key.
|
|
2
7
|
APP_KEY={{app_key}}
|
|
8
|
+
|
|
9
|
+
# Canonical base URL. Used for absolute links, and to recognise your own origin
|
|
10
|
+
# when the app runs behind a reverse proxy.
|
|
11
|
+
APP_URL=http://localhost:3000
|
|
12
|
+
|
|
13
|
+
# Connection string for the database. Must match the driver in config/database.ts.
|
|
14
|
+
#
|
|
15
|
+
# Left commented on purpose. config/database.ts already points at this file, and
|
|
16
|
+
# `bun zt test` treats a set DATABASE_URL as the test database too — so setting
|
|
17
|
+
# it here would have the suite mutate your development data instead of running
|
|
18
|
+
# against a throwaway :memory: schema. Uncomment only to point somewhere else.
|
|
19
|
+
# DATABASE_URL={{db_url}}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// independent registry that shares this one's models and sign-in.
|
|
11
11
|
import { Panel } from "@zerotal/admin";
|
|
12
12
|
import { AuthMiddleware, GuestMiddleware } from "zerotal/auth";
|
|
13
|
+
import { passwordReset } from "@app/auth/passwords";
|
|
13
14
|
|
|
14
15
|
import { ProductResource } from "@app/admin/ProductResource";
|
|
15
16
|
import { UserResource } from "@app/admin/UserResource";
|
|
@@ -35,10 +36,15 @@ Panel.configure({
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
// The panel's own login and profile screens.
|
|
39
|
+
// Login and profile come with the panel. Supplying `passwordReset` is what
|
|
40
|
+
// mounts /admin/forgot-password and /admin/reset-password and puts the "Forgot
|
|
41
|
+
// your password?" link on the login screen — without it those routes do not
|
|
42
|
+
// exist, so the link is deliberately absent rather than dead.
|
|
38
43
|
Panel.auth({
|
|
39
44
|
enabled: true,
|
|
40
45
|
heading: "{{name}}",
|
|
41
46
|
guestMiddleware: [GuestMiddleware],
|
|
47
|
+
passwordReset,
|
|
42
48
|
});
|
|
43
49
|
|
|
44
50
|
Panel.widgets(...dashboardWidgets());
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { PasswordBroker, Hash } from "zerotal/auth";
|
|
2
|
+
import { DB } from "zerotal/orm";
|
|
3
|
+
import { Log } from "zerotal/logger";
|
|
4
|
+
import { User } from "@app/models/User";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Password-reset broker for the panel's forgot/reset screens.
|
|
8
|
+
*
|
|
9
|
+
* The broker owns the security-sensitive half — generating the token, hashing
|
|
10
|
+
* it before storage, expiring it, and comparing candidates in constant time.
|
|
11
|
+
* Everything below is the part only this app can answer: where tokens live, how
|
|
12
|
+
* a message reaches the user, and what "set the password" means for our model.
|
|
13
|
+
*/
|
|
14
|
+
const broker = new PasswordBroker({
|
|
15
|
+
expireMinutes: 60,
|
|
16
|
+
|
|
17
|
+
async findToken(email) {
|
|
18
|
+
const row = await DB.table("password_reset_tokens").where("email", email).first();
|
|
19
|
+
if (!row) return null;
|
|
20
|
+
return {
|
|
21
|
+
token: String((row as Record<string, unknown>)["token"]),
|
|
22
|
+
createdAt: new Date(String((row as Record<string, unknown>)["created_at"])),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async storeToken(email, hash) {
|
|
27
|
+
// One live token per address: a second request invalidates the first rather
|
|
28
|
+
// than leaving two working links in someone's inbox.
|
|
29
|
+
await DB.table("password_reset_tokens").where("email", email).delete();
|
|
30
|
+
await DB.table("password_reset_tokens").insert({
|
|
31
|
+
email,
|
|
32
|
+
token: hash,
|
|
33
|
+
created_at: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async deleteToken(email) {
|
|
38
|
+
await DB.table("password_reset_tokens").where("email", email).delete();
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async pruneTokens(cutoff) {
|
|
42
|
+
await DB.table("password_reset_tokens").where("created_at", "<", cutoff.toISOString()).delete();
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async sendResetLink(email, token) {
|
|
46
|
+
const url = `${process.env["APP_URL"] ?? "http://localhost:3000"}/admin/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
|
|
47
|
+
|
|
48
|
+
// Logged rather than emailed, so a freshly scaffolded app has a working
|
|
49
|
+
// reset flow before any mail server exists. To send it for real, add
|
|
50
|
+
// `@zerotal/notifications` and swap this line for a Mail send — the token
|
|
51
|
+
// is the only thing this callback needs to deliver.
|
|
52
|
+
Log.info(`[password reset] ${email} → ${url}`);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async resetPassword(email, newPassword) {
|
|
56
|
+
const user = await User.query().where("email", email).first();
|
|
57
|
+
if (!user) return;
|
|
58
|
+
user.password = await Hash.make(newPassword);
|
|
59
|
+
await user.save();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adapter matching the panel's `passwordReset` contract, which wants booleans
|
|
65
|
+
* rather than the broker's result strings.
|
|
66
|
+
*/
|
|
67
|
+
export const passwordReset = {
|
|
68
|
+
async sendResetLink(email: string): Promise<boolean> {
|
|
69
|
+
// Always reports success. Answering "no such account" here would turn the
|
|
70
|
+
// forgot-password form into a way to test which addresses are registered.
|
|
71
|
+
await broker.sendResetLink(email);
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async reset(input: { email: string; token: string; password: string }): Promise<boolean> {
|
|
76
|
+
const result = await broker.reset(input.token, input.email, input.password);
|
|
77
|
+
return result === "passwords.reset";
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -3,9 +3,10 @@ import { DatabaseConfig } from "zerotal/orm";
|
|
|
3
3
|
|
|
4
4
|
export default DatabaseConfig({
|
|
5
5
|
driver: "sqlite",
|
|
6
|
-
// `bun zt test` sets
|
|
7
|
-
//
|
|
8
|
-
|
|
6
|
+
// ZT_DB_URL first: `bun zt test` sets it (defaults to :memory:) and that
|
|
7
|
+
// override has to beat the DATABASE_URL in .env, or the suite would run
|
|
8
|
+
// against your development database and leave its rows behind.
|
|
9
|
+
url: env("ZT_DB_URL", env("DATABASE_URL", "./database/db.sqlite")),
|
|
9
10
|
|
|
10
11
|
// Additive schema sync for local development — creates every demo table from
|
|
11
12
|
// the models at boot, so `bun run dev` works with no migration step. Hard-off
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Migration, Schema } from "zerotal/orm";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One live reset token per address, keyed by email rather than user id so a
|
|
5
|
+
* request for an unknown address does the same work as a known one — the
|
|
6
|
+
* response must not reveal which addresses have accounts.
|
|
7
|
+
*
|
|
8
|
+
* `token` holds a SHA-256 hash, never the value that was emailed: a leaked
|
|
9
|
+
* database should not hand out working reset links.
|
|
10
|
+
*
|
|
11
|
+
* This is a plain table rather than a model, so `synchronize` does not create
|
|
12
|
+
* it. Run `bun zt migrate` once before using password reset.
|
|
13
|
+
*/
|
|
14
|
+
export default class CreatePasswordResetTokensTable extends Migration {
|
|
15
|
+
async up(): Promise<void> {
|
|
16
|
+
await Schema.create("password_reset_tokens", (table) => {
|
|
17
|
+
table.string("email").primary();
|
|
18
|
+
table.string("token");
|
|
19
|
+
table.timestamp("created_at");
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async down(): Promise<void> {
|
|
24
|
+
await Schema.drop("password_reset_tokens");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -8,11 +8,19 @@
|
|
|
8
8
|
"dev": "bun zt.ts serve --dev",
|
|
9
9
|
"start": "bun zt.ts serve",
|
|
10
10
|
"seed": "bun zt.ts db:seed",
|
|
11
|
-
"test": "bun zt.ts test"
|
|
11
|
+
"test": "bun zt.ts test",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
15
|
"@zerotal/admin": "{{zerotal_version}}",
|
|
15
16
|
"@zerotal/flow": "{{zerotal_version}}",
|
|
16
17
|
"zerotal": "{{zerotal_version}}"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"bun": ">=1.3.14"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.8.0",
|
|
24
|
+
"@types/bun": "^1.3.14"
|
|
17
25
|
}
|
|
18
26
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
A JSON REST API — routing, ORM, authentication, validation and tests, with no frontend.
|
|
4
|
+
|
|
5
|
+
Built with [Zerotal](https://zerotal.dev) — a full-stack TypeScript framework for Bun.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- [Bun](https://bun.sh) 1.3.14 or newer. Node.js is not supported: Zerotal uses
|
|
10
|
+
Bun-native APIs throughout.
|
|
11
|
+
|
|
12
|
+
## Getting started
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun install
|
|
16
|
+
bun run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The dev server prints a local URL and a network one, so you can open the app on
|
|
20
|
+
another device on the same Wi-Fi.
|
|
21
|
+
|
|
22
|
+
## Scripts
|
|
23
|
+
|
|
24
|
+
| Command | What it does |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `bun run dev` | Start the dev server with hot reload |
|
|
27
|
+
| `bun run start` | Start the server without dev tooling |
|
|
28
|
+
| `bun run test` | Run the test suite |
|
|
29
|
+
| `bun run typecheck` | Type-check without emitting |
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Environment variables live in `.env`, documented in `.env.example`. `APP_KEY`
|
|
34
|
+
was generated for this project when it was scaffolded — keep it out of version
|
|
35
|
+
control, and use a different one per environment.
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
Full documentation is at [zerotal.dev/docs](https://zerotal.dev/docs).
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
# The runtime environment. "production" turns on the framework's stricter
|
|
2
|
+
# config checks and turns off verbose error output.
|
|
1
3
|
APP_ENV=development
|
|
4
|
+
|
|
5
|
+
# Signs sessions, cookies and encrypted values. Generated per project — never
|
|
6
|
+
# reuse one between environments, and never commit a real key.
|
|
2
7
|
APP_KEY={{app_key}}
|
|
8
|
+
|
|
9
|
+
# Canonical base URL. Used for absolute links, and to recognise your own origin
|
|
10
|
+
# when the app runs behind a reverse proxy.
|
|
11
|
+
APP_URL=http://localhost:3000
|
|
12
|
+
|
|
13
|
+
# Connection string for the database. Must match the driver in config/database.ts.
|
|
3
14
|
DATABASE_URL={{db_url}}
|
|
15
|
+
|
|
16
|
+
# Separate secret for session payloads, independent of APP_KEY.
|
|
4
17
|
SESSION_SECRET={{session_secret}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HttpContext } from '
|
|
2
|
-
import { Hash } from '
|
|
1
|
+
import type { HttpContext } from 'zerotal';
|
|
2
|
+
import { Hash } from 'zerotal/auth';
|
|
3
3
|
import { User } from '../models/User.ts';
|
|
4
4
|
|
|
5
5
|
export class AuthController {
|
|
@@ -19,12 +19,15 @@ export class AuthController {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
// `role` is not in `fillable`, so it cannot be mass-assigned — passing it to
|
|
23
|
+
// create() is refused outright, which is the guard doing its job. Setting it
|
|
24
|
+
// here, in code, is the point: a value the request can never influence.
|
|
25
|
+
const user = new User();
|
|
26
|
+
user.name = name;
|
|
27
|
+
user.email = email;
|
|
28
|
+
user.password = await Hash.make(password);
|
|
29
|
+
user.role = 'user';
|
|
30
|
+
await user.save();
|
|
28
31
|
|
|
29
32
|
http.session?.set('user_id', user.id);
|
|
30
33
|
http.response = Response.json({ data: { id: user.id, name: user.name, email: user.email } }, { status: 201 });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseModel, column, table } from '
|
|
1
|
+
import { BaseModel, column, table } from 'zerotal/orm';
|
|
2
2
|
|
|
3
3
|
@table('users')
|
|
4
4
|
export class User extends BaseModel {
|
|
@@ -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' }) role
|
|
15
|
+
@column({ type: 'string', nullable: true, default: 'user' }) role?: string;
|
|
16
16
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Application } from '
|
|
2
|
-
import { DatabaseProvider } from '
|
|
3
|
-
import { SessionProvider } from '
|
|
4
|
-
import { AuthProvider } from '
|
|
1
|
+
import { Application } from 'zerotal';
|
|
2
|
+
import { DatabaseProvider } from 'zerotal/orm';
|
|
3
|
+
import { SessionProvider } from 'zerotal/session';
|
|
4
|
+
import { AuthProvider } from 'zerotal/auth';
|
|
5
5
|
import { NotificationProvider } from '@zerotal/notifications';
|
|
6
|
-
import { QueueProvider } from '
|
|
7
|
-
import { LogProvider } from '
|
|
6
|
+
import { QueueProvider } from 'zerotal/queue';
|
|
7
|
+
import { LogProvider } from 'zerotal/logger';
|
|
8
8
|
|
|
9
9
|
export default Application.create({
|
|
10
10
|
providers: [
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { env } from '
|
|
2
|
-
import { DatabaseConfig } from '
|
|
1
|
+
import { env } from 'zerotal';
|
|
2
|
+
import { DatabaseConfig } from 'zerotal/orm';
|
|
3
3
|
|
|
4
4
|
export default DatabaseConfig({
|
|
5
|
-
|
|
5
|
+
// The driver has to agree with the URL's protocol. Boot validation rejects a
|
|
6
|
+
// sqlite driver pointed at postgres:// or mysql://, which is exactly what you
|
|
7
|
+
// get if this is left to its default while DATABASE_URL names a network
|
|
8
|
+
// database — so it is stamped from the database you chose when scaffolding.
|
|
9
|
+
driver: '{{db_driver}}',
|
|
10
|
+
|
|
11
|
+
// ZT_DB_URL first: `bun zt test` sets it (defaults to :memory:) and that
|
|
12
|
+
// override has to beat the DATABASE_URL in .env, or the suite would run
|
|
13
|
+
// against your development database and leave its rows behind.
|
|
14
|
+
url: env('ZT_DB_URL', env('DATABASE_URL', '{{db_url}}')),
|
|
6
15
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { env } from '
|
|
2
|
-
import { SessionConfig } from '
|
|
1
|
+
import { env } from 'zerotal';
|
|
2
|
+
import { SessionConfig } from 'zerotal/session';
|
|
3
3
|
|
|
4
4
|
export default SessionConfig({
|
|
5
5
|
driver: env('SESSION_DRIVER', 'cookie') as 'cookie' | 'redis',
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { Migration, Schema } from '
|
|
1
|
+
import { Migration, Schema } from 'zerotal/orm';
|
|
2
2
|
|
|
3
3
|
export default class CreateUsersTable extends Migration {
|
|
4
4
|
async up(): Promise<void> {
|
|
5
|
-
await Schema.create('users', (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
await Schema.create('users', (table) => {
|
|
6
|
+
table.increments('id');
|
|
7
|
+
table.string('name');
|
|
8
|
+
table.string('email').unique();
|
|
9
|
+
table.string('password');
|
|
10
|
+
table.string('role').nullable().default('user');
|
|
11
|
+
table.timestamps();
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
"zt": "bun zt.ts",
|
|
8
8
|
"dev": "bun zt.ts serve --dev",
|
|
9
9
|
"start": "bun zt.ts serve",
|
|
10
|
-
"test": "bun zt.ts test"
|
|
10
|
+
"test": "bun zt.ts test",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@zerotal/notifications": "{{zerotal_version}}",
|
|
14
15
|
"zerotal": "{{zerotal_version}}"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"bun": ">=1.3.14"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.8.0",
|
|
22
|
+
"@types/bun": "^1.3.14"
|
|
15
23
|
}
|
|
16
24
|
}
|
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import { Router } from '
|
|
1
|
+
import { Router } from 'zerotal';
|
|
2
2
|
import { WelcomeController } from '../app/controllers/WelcomeController.ts';
|
|
3
3
|
import { AuthController } from '../app/controllers/AuthController.ts';
|
|
4
4
|
import { RequireAuth } from '../app/middleware/RequireAuth.ts';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Every route this API serves.
|
|
8
|
+
*
|
|
9
|
+
* Exported as a function *and* called below, because the two callers need
|
|
10
|
+
* different things. `bootstrap/app.ts` points `.routing()` at this file and
|
|
11
|
+
* relies on importing it to register the routes — that is the call at the
|
|
12
|
+
* bottom. Tests build their own application, and `createTestApp` resets the
|
|
13
|
+
* router before handing control to its `setup` callback; re-importing this file
|
|
14
|
+
* would not help, since a module's top-level code runs once per process. So the
|
|
15
|
+
* suite calls `registerRoutes()` from that callback instead.
|
|
16
|
+
*/
|
|
17
|
+
export function registerRoutes(): void {
|
|
18
|
+
// Public
|
|
19
|
+
Router.get('/', WelcomeController, 'index');
|
|
20
|
+
Router.post('/register', AuthController, 'register');
|
|
21
|
+
Router.post('/login', AuthController, 'login');
|
|
22
|
+
Router.post('/logout', AuthController, 'logout');
|
|
11
23
|
|
|
12
|
-
// Protected
|
|
13
|
-
Router.get('/me', AuthController, 'me', [RequireAuth]);
|
|
24
|
+
// Protected
|
|
25
|
+
Router.get('/me', AuthController, 'me', [RequireAuth]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
registerRoutes();
|
|
@@ -5,14 +5,17 @@ import {
|
|
|
5
5
|
assertDatabaseHas,
|
|
6
6
|
assertDatabaseMissing,
|
|
7
7
|
type TestApp,
|
|
8
|
-
} from '
|
|
9
|
-
import '../routes/index.ts';
|
|
8
|
+
} from 'zerotal/testing';
|
|
9
|
+
import { registerRoutes } from '../routes/index.ts';
|
|
10
10
|
import { createApp } from './helpers.ts';
|
|
11
11
|
|
|
12
12
|
let app: TestApp;
|
|
13
13
|
|
|
14
14
|
beforeAll(async () => {
|
|
15
|
-
|
|
15
|
+
// Routes are registered through the setup callback, not by importing this
|
|
16
|
+
// file for its side effects: `createTestApp` resets the router before it
|
|
17
|
+
// runs setup, and a module's top-level code only executes once per process.
|
|
18
|
+
app = await createApp(registerRoutes);
|
|
16
19
|
// Build the schema from the same migrations that ship, rather than from a
|
|
17
20
|
// second copy of the tables written by hand — the two always drift.
|
|
18
21
|
await migrateDatabase();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Application } from '
|
|
2
|
-
import { DatabaseProvider } from '
|
|
3
|
-
import { SessionProvider, AuthSessionMiddleware, CookieDriver } from '
|
|
4
|
-
import { AuthProvider } from '
|
|
1
|
+
import { Application } from 'zerotal';
|
|
2
|
+
import { DatabaseProvider } from 'zerotal/orm';
|
|
3
|
+
import { SessionProvider, AuthSessionMiddleware, CookieDriver } from 'zerotal/session';
|
|
4
|
+
import { AuthProvider } from 'zerotal/auth';
|
|
5
5
|
import { NotificationProvider } from '@zerotal/notifications';
|
|
6
|
-
import { QueueProvider } from '
|
|
7
|
-
import { createTestApp, type TestApp } from '
|
|
6
|
+
import { QueueProvider } from 'zerotal/queue';
|
|
7
|
+
import { createTestApp, type TestApp } from 'zerotal/testing';
|
|
8
8
|
import { User } from '../app/models/User.ts';
|
|
9
9
|
|
|
10
10
|
export const TEST_SESSION_SECRET = 'test-secret';
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
"exactOptionalPropertyTypes": true,
|
|
8
8
|
"noUncheckedIndexedAccess": true,
|
|
9
9
|
"emitDecoratorMetadata": false,
|
|
10
|
-
"skipLibCheck": true
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"allowImportingTsExtensions": true
|
|
11
13
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
+
"exclude": [
|
|
15
|
+
"node_modules",
|
|
16
|
+
".zerotal"
|
|
17
|
+
]
|
|
14
18
|
}
|