0.1.0GitHub
Getting StartedStarter Kit

Getting Started

Starter Kit

On this page

Introduction

The starter kit is what pnpm create marmeon gives you by default: a blank app plus two modules. The auth module handles accounts: sign-in, registration, password reset, the confirmation of an address, a security page and API tokens. The home module is the page at /. Both are ordinary modules in your app, written as source code, so you read them, change them and delete what you do not need.

pnpm create marmeon my-app          # the starter kit
pnpm create marmeon my-app --minimal  # a blank app, without both modules

The auth module comes from the generator marmeon make:auth. It writes the module once, and from then on it belongs to your app. The mechanics behind it, such as hashing, sessions, tokens and policies, stay in the @marmeon/auth package, and updates of the framework bring them.

What the auth module holds

PartWhat it is
index.tsThe module's definition: its provider, the listener that sends the confirmation mail, the queued jobs, the migrations, the seeder, the routes, the schedule and the auth:prune-unverified command.
AuthServiceProvider.tsFinds users through the UserRepository, shares auth with every page, and sets the rate limits of its forms.
users.ts, UserRepository.tsThe User type and the only code that reads a password hash.
routes.ts, api-routes.tsThe pages and forms, and GET /api/user for API tokens.
controllers/24 controllers, one per endpoint. A form's controller validates it with a request schema.
views/, layouts/The React pages and the app's layout, AppLayout, with a settings layout inside it.
mail/Six mails: the confirmation of an address, the reset link, the notes about a change of address and about a known address.
jobs/, listeners/, tasks/The work that runs on the queue or on a schedule.
middleware/ShareAuthProps and ConfirmedAddress.
migrations/The tables users, remember_tokens, password_reset_tokens and personal_access_tokens.
factories/, seeders/UserFactory for tests, and UserSeeder with test@example.com and the password password. The seeder runs only with marmeon db:seed or migrate:fresh --seed: never seed a server with it.
schedule.tsThe nightly clean-up of expired tokens and changes of address.
auth.test.ts and three moreThe module's own tests.

apps/web-app in the framework's repository shows such a module grown further, with a welcome mail and German texts.

The pages

PathNameWho
/loginauth.loginguests
/registerauth.registerguests
/register/sentauth.register.sentguests, after they register
/forgot-password, /reset-passwordauth.password.request, auth.password.resetguests
/dashboarddashboardsigned in, with a confirmed address
/confirm-passwordauth.password.confirmsigned in, with a confirmed address
/settings/securityauth.securitythe same, and the password typed within 15 minutes
/settings/tokensauth.tokensthe same
/email/verifyauth.verification.noticesigned in

A signed-in user who opens a guest page goes to /dashboard. The security page changes the password and the e-mail address, and signs out the other devices. The tokens page creates and revokes API tokens. / belongs to the home module, so the auth module leaves it free.

The routes are an ordinary route file, so you change a path, add a page or remove one in modules/auth/routes.ts. The routing page explains route files.

Private registration

The registration never tells anyone whether an address already has an account. A new address and a known one get the same answer in the same time: a redirect to /register/sent, which asks the person to check their inbox. The owner of a known address gets a mail instead of the confirmation link: someone tried to sign up with it, and here is how to sign in or reset the password. This keeps a stranger from testing a list of addresses against your users.

Three more rules follow from it:

  • Nobody is signed in by registering. The new account signs in like any other. On the pages of the auth module it sees only a notice until its owner clicks the link in the mail while signed in. Whoever registers with somebody else's address cannot confirm it, and the real owner takes the account over with a password reset.
  • A form field never asks the database. The live check of the form only looks at the address's format.
  • A change of address waits for its link. A new address on the security page is kept aside until the link sent to it is clicked, and the answer is the same for a free and a taken address. The owner of a taken address gets a note instead, and nothing changes. A pending change expires after 60 minutes.

The price is that someone who forgot they have an account learns it only from their inbox, and nobody can start before the mailer and a queue worker run.

Your own pages

The notice guards the pages of the auth module alone: its routes pass its ConfirmedAddress middleware. A route of another module behind authenticate() alone lets in every signed-in account, also one whose address is not confirmed, such as one registered with somebody else's address. Add verified() of @marmeon/auth after authenticate():

modules/notes/routes.ts
import { authenticate, verified } from '@marmeon/auth';
import { defineRoutes } from '@marmeon/http';
import { ListNotesController } from './controllers/ListNotesController.ts';

export default defineRoutes((Route) => {
  Route.middleware(authenticate()).middleware(verified()).get('/notes', ListNotesController).name('notes.index');
});

verified() sends an account without a confirmed address to the notice, /email/verify, and answers 403 to a request that expects JSON. It asks for a confirmed address whatever AUTH_PRIVATE_REGISTRATION says.

Turning it off

Set AUTH_PRIVATE_REGISTRATION=false when the accounts of your app are public anyway:

.env
AUTH_PRIVATE_REGISTRATION=false

Then a taken address is a field error when the form is sent, and a new account is signed in at once. The confirmation mail still goes out, but the app's pages no longer wait for it. A new address on the security page takes effect at once. /register/sent and the change-of-address link answer 404.

Only true and false are allowed. Any other value stops the start; an empty one counts as unset. To demand a confirmed address with private registration off, put verified() of @marmeon/auth in place of ConfirmedAddress in routes.ts. Set AUTH_REVOKE_TOKENS_ON_PASSWORD_CHANGE=true as well: an account registered with somebody else's address may have created an API token, and the owner's password reset then ends it, as the API tokens page explains.

Mail, the queue and the scheduler

Every mail of the module goes through the queue, on its mail queue. A new app has the queue's table and marmeon dev starts a worker, so this works from the first start. By default the mailer writes each mail to the log. On a server you need a real mailer, MAIL_MAILER=smtp, and a worker, marmeon queue:work. The deployment page shows both.

The scheduler deletes expired reset links, remember-me tokens and API tokens every night at 03:10, and the changes of address that nobody confirmed in time. marmeon dev runs the scheduler for you. On a server, run marmeon schedule:work.

Accounts nobody confirmed

marmeon auth:prune-unverified deletes the accounts whose address was never confirmed, once AUTH_UNVERIFIED_DAYS days have passed since they signed up. It ends a registration with somebody else's address, and it keeps no data of sign-ups nobody finished. An account that was confirmed once is never deleted.

The command is not scheduled by itself, because deleting accounts is your app's decision. Turn its line on in modules/auth/schedule.ts:

modules/auth/schedule.ts
import { defineSchedule } from '@marmeon/scheduler';
import { PruneEmailChanges } from './tasks/PruneEmailChanges.ts';

export const schedule = defineSchedule((s) => {
  s.command('auth:prune-tokens').daily().at('03:10').onOneServer();
  s.call(PruneEmailChanges).daily().at('03:10').onOneServer().name('auth.prune-email-changes');
  s.command('auth:prune-unverified').daily().at('03:20').onOneServer();
});

onOneServer() runs a task on one server only, as long as the servers share a cache: CACHE_DRIVER=database or redis.

Configuration

VariableDefaultEffect
AUTH_PRIVATE_REGISTRATIONtruefalse makes the registration and the change of address say "taken", and signs a new account in at once.
AUTH_UNVERIFIED_DAYS7Days an account may stay without a confirmed address before auth:prune-unverified deletes it. 0: the command deletes none.
AUTH_REVOKE_TOKENS_ON_PASSWORD_CHANGEfalsetrue: a new password and a password reset revoke every API token of the user. Set it with AUTH_PRIVATE_REGISTRATION=false.

The authentication page lists the other AUTH_* variables, such as how long "remember me" lasts.

Adding the module to an existing app

An app made with --minimal gets the module later from the generator:

pnpm marmeon make:auth

The command copies the module to modules/auth/, adds it to modules in bootstrap/app.ts and imports its stylesheet, modules/auth/auth.css, in bootstrap/client.tsx. It names every package the module needs that your package.json lacks, and any that is only a dev dependency: the module runs in production, so those belong in dependencies. When nothing is missing, it writes the route types. It needs config/app.ts with AppConfig, which every new app has.

After it, run pnpm marmeon migrate, and set the MAIL_* variables before anyone else uses the app. An existing modules/auth/ stays as it is unless you pass --force, which overwrites it with the templates. --adapter=react names the UI adapter, and React is the only one with templates.

Testing

The module brings its own tests: sign-in, registration with both values of AUTH_PRIVATE_REGISTRATION, the change of address and the deletion of unconfirmed accounts. They run with the app's other tests:

pnpm test

UserFactory makes users in your own tests, with the password password, and #modules/auth exports it. The HTTP tests page shows how to act as a signed-in user.