What is UniAuth?

UniAuth is a separate PostgreSQL database that stores user credentials (email, hashed password, verified status). Multiple Vesta apps can share the same UniAuth database, giving users a single login across all your services.

It connects via PostgreSQL Foreign Data Wrapper (postgres_fdw), so your app's main database can query UniAuth tables as if they were local.

Setup

Add a [UNIAUTH] section to server.ini:

[UNIAUTH]
DB_NAME = uniauth
DB_HOST = localhost
DB_PORT = 5432

The UniAuth database must be created and initialised separately (it's shared infrastructure). Your app inherits from Server — UniAuth endpoints are automatically exposed by the base class.

Auto-exposed routes

When using Server, these routes exist without any code in your app:

RouteDescription
GET /authServe the login/register page (static/auth.html)
POST /loginLogin or register a user
GET /logoutDelete JWT cookies and redirect to /auth
GET /verifServe the email verification page
POST /signupVerify email with OTP code
POST /resendVerifResend the OTP email
GET /resetServe the password reset page
POST /passwordResetRequest a reset email
POST /changePasswordVerifReset password with OTP code
POST /goodbyePermanently delete the account

Login & register flow

The /login endpoint handles both new and returning users with the same call. The frontend sends email + password (and optionally a referral code):

// Frontend — same request for login and register
fetch('/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password, parrain: referralCode }),
})

The server responds with a string:

ResponseMeaning
"ok"Existing account, correct password — JWT set, redirected to DEFAULT_ENDPOINT
"verif"New account created — OTP email sent, redirected to /verif
"invalid email or password"Account exists but password is wrong

After a successful login, the server:

  1. Creates a JWT signed with SECRET_KEY, valid for 24 hours.
  2. Sets two cookies: JWT (HttpOnly) and auth (readable by JS).
  3. Calls self.onLogin(uid) — override this hook in your app.
  4. Redirects to DEFAULT_ENDPOINT from server.ini.

onLogin hook

Override onLogin to run logic the first time a user logs in — for example to create their profile row in your database.

class App(Server):

    def onLogin(self, uid):
        # uid is the UniAuth user ID (integer)
        existing = self.db.getSomething("profiles", uid)
        if not existing:
            self.db.insertDict("profiles", {
                "id":         uid,
                "created_at": "NOW()",
                "plan":       "free",
            })

Checking authentication

self.getUser(verif=False)
int — the current user's ID
Validate the JWT cookie and return the user ID.
  • No JWT / expired → redirect to /auth
  • Valid JWT, user not verified → redirect to /verif
  • Valid JWT, user verified → return user ID ✓

verif=True inverts the verification check — it is intended for use on the /verif page itself: it lets unverified users through but redirects already-verified users away to DEFAULT_ENDPOINT. Do not use verif=True in API routes.
@Server.expose
def api_dashboard(self):
    uid = self.getUser()   # standard usage in every protected route
    return json.dumps({"uid": uid})

# verif=True is used internally by the /verif and /resendVerif routes —
# it allows access when unverified and redirects away when already verified.
# You should not need it in your own routes.
self.checkJwt(verif=False)
dict — JWT payload
Like getUser() but returns the full decoded payload {"username": uid, "verified": bool, "exp": datetime} instead of just the ID. Raises on failure.

JWT internals

Tokens are issued with PyJWT using HS256 and expire after 24 hours.

CookieHttpOnlyContents
JWT Yes Full signed token — only the server reads this.
auth No The string "true" — readable by JS as a simple logged-in flag. Does not contain the token itself.
self.createJwt(uid, verified)
None — sets cookies as a side-effect
Manually issue a JWT for a user. Normally called automatically by /login. Useful if you implement a custom SSO flow.

Email verification

New accounts are created in an unverified state. Vesta sends a 6-digit OTP to the user's email immediately after registration.

  1. User registers → OTP email sent → redirected to /verif.
  2. User enters the code → POST /signup?code=123456.
  3. On success → account is marked verified → redirected to DEFAULT_ENDPOINT.
self.sendVerification(uid, mail="")
None
Generate a fresh OTP and email it to the user. The OTP expires in 1 hour. Called automatically on registration; call manually to resend.

The OTP email uses mailing/mailVerif.html. Customise this template to match your brand.

Password reset

  1. User submits email → POST /passwordReset?email=…
  2. Vesta emails a 12-character code (valid 1 hour) using mailing/mailReset.html.
  3. User submits new password + code → POST /changePasswordVerif?mail=…&code=…&password=…
  4. Password is rehashed with bcrypt and stored. User is redirected to /auth.

The reset page is served by GET /reset?email=… from static/reset.html.

Account management

Logout

GET /logout deletes both cookies and redirects to /auth. On the frontend:

import { logout } from '/static/framework/vesta.mjs';
logout(); // POST /logout then redirects

Delete account

POST /goodbye permanently removes the user from UniAuth. You should handle cleanup of app-specific data in onGoodbye if needed.

import { goodbye } from '/static/framework/vesta.mjs';
goodbye(); // asks for confirmation, then POSTs /goodbye

Low-level UniAuth DB methods

These are called internally. You can use them directly for custom auth flows.

db.getUserCredentials(email)
dict | None — {id, email, password_hash, verified}
Fetch a user row from the UniAuth database by email.
db.getUser(id)
dict | None
Fetch a user row from the UniAuth database by ID.
db.createAccount(email, password, parrain)
int — new user ID
Create a new UniAuth account. password is the plaintext password — it is hashed with bcrypt before storage. parrain is an optional referral code.

Referral codes

Pass a parrain query parameter to /auth and it will be forwarded to /login automatically:

<a href="/auth?parrain=INVITE42">Join with invite</a>

The code is passed to db.createAccount(email, password, parrain) and stored in UniAuth. Handle rewards or access-gating in onLogin.