Auth and mail
Passwords, codes by email, two factors, password reset, email change, sign in alerts, the rate limit and the mails, all declared in schema.ts.
An auth collection is a collection whose records sign in. Every option of it lives in schema.ts, beside its fields. The server checks the options at boot, as PocketBase does, and refuses a combination that cannot work.
Options in schema.ts#
import { auth, bool, mailTemplatesFr, text } from 'locosdk/schema'
const fr = mailTemplatesFr({ otpSeconds: 600 })
users: auth({
fields: { name: text(), role: text(), disabled: bool() },
indexes: ['CREATE UNIQUE INDEX idx_users_name ON users (name)'],
authRule: 'disabled = false',
options: {
passwordAuth: { identityFields: ['email', 'name'] },
otp: { enabled: true, duration: 600, length: 6, emailTemplate: fr.otp },
mfa: { enabled: true, rule: "role = 'admin'" },
authAlert: { enabled: true },
authToken: { duration: 7 * 86400 },
resetPasswordTemplate: fr.resetPassword,
},
}),
- Every key is optional. A key left out is the default, so removing a line turns the option back to its default.
- A schema never holds a token secret: the type has no slot for one. Secrets are minted once in the database and survive every boot.
- The boot refuses: two factors with fewer than two methods on, an identity field without a single column unique index, a mail template with an empty subject or body, a duration outside its range.
Defaults#
| what | default |
|---|---|
| auth token | 12 hours, refreshable |
| file token | 180 seconds |
| verification link | 1 day |
| password reset link, email change link | 30 minutes |
| code by email | 8 digits, 180 seconds |
| two factor session | 600 seconds |
| password | 8 characters minimum, 71 maximum, bcrypt cost 10 |
Signing in#
- Password:
POST /api/collections/users/auth-with-passwordwithidentityandpassword. The identity fields areemailby default. - Code by email:
request-otpmails a code and answers anotpId, also for an unknown address, so nobody can probe accounts.auth-with-otpsigns in with it. Wrong codes are limited to 5 per 180 seconds per account. - Two factors: with
mfa.enabled, a first factor answers 401{ mfaId }whenmfa.rulematches the record. The second factor sends thatmfaIdwith a DIFFERENT method. The client'sauthWithPasswordreturns{ mfaId }in its type, andSessionopens the code step by itself (Client and data layer). - The auth rule runs after the factors:
authRule: 'disabled = false'refuses a switched off account with 403. - Superusers sign in on
_superusers.golo superuser upsert <email>creates one or sets its password, from stdin.
Changing a password or an email#
- A user changing their own password sends
oldPassword. Without it, or with a wrong one, the update fails: a stolen session cannot set a new password. request-password-resetmails a link (at most one per 2 minutes) and answers the same whether the address exists or not.confirm-password-resetsets the password and ends every session of the account.request-email-changemails a link to the NEW address.confirm-email-changetakes the link's token and the account password, sets the email andverified, and ends every session.- A password or email change rotates the record's
tokenKey. Every token of the record dies at once, codes and MCP access tokens included.
Sign in alerts#
With authAlert.enabled (the default), a password or code sign in from a new device mails the account an alert with the time, the IP and the browser. The first sign in only records its device. An account keeps its 5 newest devices. A failed alert is logged and never blocks the sign in.
The rate limit#
Password and code sign ins are limited to 5 per 3 seconds per client IP, then 429 Too Many Requests. Behind a proxy, set trustedProxy so the limit sees the real client and not the proxy (Files, backups and proxies). createApp({ rateLimits }) takes PocketBase's rate limit rules for other labels.
Mail#
import { createApp, smtpTransport } from 'locosdk/server'
const app = createApp({
schema,
meta: { appName: 'Orders', appURL: 'https://orders.example.com', senderName: 'Orders', senderAddress: 'no-reply@example.com' },
mail: smtpTransport({ host: 'smtp.example.com', port: 587, username: 'orders', password: process.env.SMTP_PASSWORD ?? '' }),
})
- The SMTP client is built in, with no dependency: STARTTLS when the server offers it, implicit TLS with
tls: true, AUTH PLAIN or LOGIN. It refuses to send a password over plain text, except to the local machine. smtpProbe(config)connects, authenticates and quits:{ ok: true }or{ ok: false, error }, never the password.- Five mails per auth collection:
otp,authAlert,resetPassword,verification,confirmEmailChange. English is PocketBase's text. French comes frommailTemplatesFr({ otpSeconds }). - Placeholders:
{APP_NAME},{APP_URL},{TOKEN},{OTP},{OTP_ID},{ALERT_INFO}, and{RECORD:<field>}for any field that is not hidden. - Every mail goes through
onMailerSend, where a hook can add a footer or change the recipients.