Building apps
App accounts
How people sign up and sign in to a generated app, what a session is, and the auth endpoints behind it.
Overview
Ripping Auth gives each app its own accounts: email and password, or Google. Accounts belong to the project (app_users), not to Ripping's own users. The app keeps a session token in localStorage under auth.token and sends it as Authorization: Bearer <token> to every route under /api/apps/<projectId>/.
SITE.accounts: false removes all of this: no sign-in screens, no useAuth state, no per-account data.
useAuth
type User = { id: string; email: string; name: string | null; provider: "password" | "google"; role?: "admin" | "member" };
function useAuth(): {
user: User | null; loading: boolean;
signIn: typeof signIn; signUp: typeof signUp; signInWithGoogle: typeof signInWithGoogle; signOut: typeof signOut;
requestPasswordReset: typeof requestPasswordReset; updateProfile: typeof updateProfile;
changePassword: typeof changePassword; deleteAccount: typeof deleteAccount;
};
loading is true until the saved token has been checked. On first use the app calls GET auth/me; if the network is down it decodes the user from the token instead, so the app opens signed in while offline. role only arrives from me, so it is missing offline.
role is "admin" for people the owner marked as admins on the project's Users tab on Ripping. Admins see every inbox record and every upload, and may edit or delete any record.
Functions
function signIn(email: string, password: string): Promise<void>;
function signUp(email: string, password: string, name?: string): Promise<void>;
function signInWithGoogle(): void;
function signOut(): void;
function requestPasswordReset(email: string): Promise<void>; // resolves the same way whether or not the account exists
function updateProfile(p: { name: string }): Promise<void>;
function changePassword(currentPassword: string, password: string): Promise<void>;
function deleteAccount(password?: string): Promise<void>; // password accounts must confirm with the password
function getToken(): string | null;
Each one throws an Error whose message is the server's error field, or Something went wrong. Try again. when there was none. A 401 on a signed-in call (update, delete) clears the token and sets user to null.
signInWithGoogle opens a 480×640 popup at GET auth/google?return=<current page>; when the popup is blocked it navigates the page instead. The callback posts { source: "ripping-auth", token, user } to the opener, or sends the browser back to the return address with #ripping_token=<token>, which auth.ts reads and strips on start.
signOut only forgets the token. Sessions are stateless; there is nothing to sign out of on the server.
Saving or clearing a token fires a window event named auth:change. store.ts, shared.ts and files.ts listen for it and switch to the new account's data.
Sessions
A session token is a JWT signed with HS256 using a key derived per project. Its payload carries sub (user id), pid (project id), email, name, provider, ver (the account's token version), iat and exp.
- A token lasts 30 days from issue.
- It is bound to the project: a token from one app is refused by another.
- The server checks the account on every request that needs one: the account must exist, be
active, and itstoken_versionmust equal the token'sver. Resetting a password bumps the version, so every older session stops working at once. Blocking an account on Ripping does the same.
Sign-up modes
The owner chooses who may create an account. The mode is stored on the project (app_signup) and read on every sign-up.
| Mode | What happens on sign-up |
|---|---|
open (default) | The account is created active and signed in. |
invite | Only an email the owner invited may sign up; the invite is marked accepted. Anyone else gets This app is invite-only. Ask its owner for an invite. |
approval | An invited email is active at once. Any other email is created pending and gets Thanks for signing up. The owner of this app approves new accounts; you'll get an email when you can sign in. A pending account cannot sign in until approved. |
A blocked account gets This account can't sign in to this app. on sign-in.
The owner's plan also caps users per app. When the cap is reached, sign-up fails with This app isn't taking new accounts right now.
Password rules
checkPassword runs on the server for sign-up, reset and change. It refuses:
- fewer than 8 characters (
Use at least 8 characters.) - one of a short list of common passwords
- a password containing the local part of the person's email, when that part is 4+ characters (
Do not put your name or email in your password.) - one repeated character, or a keyboard run such as
qwertyui - anything scoring 1 out of 4: short and low-variety (
This would not take long to guess. Make it longer — a few ordinary words together works better than a short jumble.)
Length counts more than variety: 16 characters of anything scores 4.
Endpoints
All at POST /api/apps/<projectId>/auth/<action> with a JSON body, except me and google which are GET. CORS is open. Every error is { "error": "<message>" }.
Rate limits
Per IP per minute: signup 10, forgot 5, reset 10, delete 10, everything else 20. Over the limit: 429 Too many attempts. Try again in a minute.
Per email address per hour, for signin, forgot and reset: 10. Over the limit: 429 Too many attempts for this account. Try again later.
me
GET /api/apps/<projectId>/auth/me
Authorization: Bearer <token>
{ "user": { "id": "…", "email": "ada@example.com", "name": "Ada", "provider": "password", "role": "member" } }
A deleted, blocked or pending account, an expired token or a bumped version returns 401 { "user": null }.
signup
POST /api/apps/<projectId>/auth/signup
Content-Type: application/json
{ "email": "ada@example.com", "password": "correct horse battery", "name": "Ada" }
{ "token": "<jwt>", "user": { "id": "…", "email": "ada@example.com", "name": "Ada", "provider": "password" } }
name is trimmed to 80 characters. Failures: 400 Enter a valid email., 400 <password problem>, 409 That email already has an account. Sign in instead., 403 <sign-up mode message>, 403 <pending message>.
signin
POST /api/apps/<projectId>/auth/signin
{ "email": "ada@example.com", "password": "…" }
Same reply as signup. Failures: 400 Enter a valid email., 401 Wrong email or password., 403 when the account is pending or blocked.
forgot
POST /api/apps/<projectId>/auth/forgot
{ "email": "ada@example.com", "return": "https://myapp.example/#/signin" }
{ "ok": true }
Always ok, so the form cannot reveal who has an account. When the account exists, an email carries a link to a page Ripping hosts (/apps/<projectId>/reset?token=…) that works for one hour. return is only kept when it points at the app's own address; the reset page then offers a way back.
reset
POST /api/apps/<projectId>/auth/reset
{ "token": "<from the email>", "password": "a new long password" }
Signs in with a fresh token and a bumped version. Failures: 400 This reset link is incomplete., 400 <password problem>, 400 This reset link has expired or was already used. Ask for a new one., 403 when the account is pending or blocked.
update
POST /api/apps/<projectId>/auth/update
Authorization: Bearer <token>
{ "name": "Ada L." }
or { "currentPassword": "…", "password": "…" }. Returns a new { token, user }. Failures: 401 Sign in again to do that., 401 That account no longer exists., 400 Your current password is wrong., 400 <password problem>, 400 Nothing to change., 500 Could not save. Try again.
delete
POST /api/apps/<projectId>/auth/delete
Authorization: Bearer <token>
{ "password": "…" }
{ "ok": true }
Deletes the account and its app_data rows (cascade). A password account must send the password: 400 That password is wrong. Other failures: 401 Sign in again to do that., 500 Could not delete the account. Try again.
GET /api/apps/<projectId>/auth/google?return=<absolute URL>
Redirects to Google, then back. return must be https and match one of: the project's published URL origin, a verified custom domain, its publish subdomain, or localhost (where http is allowed). Anything else gets a 400 page: This sign-in can only return to the app's own address. Open the app from its published link and try again. When no Google client is configured the page is 503 Google sign-in isn't configured on this platform yet. The Google account's email must be verified. The owner may supply their own Google client on Ripping; otherwise Ripping's is used.