Skip to content

🔓 Authentication

Permissions

The server supports fine grained permission control.

Each user has a role that has a specific set of permissions:

apps/server/src/api/auth/types.ts
export type AccessRights = Readonly<{
editSiteMetadata: boolean, // Permission to edit Site metadata (Name of the site etc...)
admin: boolean, // Permission for general administration tasks
editOwnUsername: boolean, // Permission to edit own username
editRoles: boolean, // Permission to create edit and delete Roles
editSchemas: boolean, // Permission to create, edit, and delete Schemas
createShare: boolean, // Permission to create Share Tokens
readableCollections: CollectionLevelPermission, // Collection IDs of collections that are readable
writableCollections: CollectionLevelPermission, // Collection IDs of collections that are writable
readableEntities: EntityLevelPermission, // Entity IDs of entities that are readable
writableEntities: EntityLevelPermission, // Entity IDs of entities that are writable
readMedia: boolean, // Permission to read Media (See Images and Videos that are uploaded)
deleteMedia: boolean, // Permisison to read Media
uploadMedia: boolean, // Permission to upload new Media
}>

Collection Level Permissions

This type defines permissions at the collection level. It can store a list of allowed collection IDs or use a wildcard (*) to grant access to all collections.

apps/server/src/api/auth/types.ts
export type CollectionLevelPermission = ReadonlySet<string> | '*'

Entity Level Permission

This type allows granular control over entity-level permissions within a collection. It can:

  • Use a wildcard (*) to grant access to all entities.
  • Specify specific entity IDs using specificEntities.
  • Grant access to all entities within a particular collection using entitiesFromCollection.
apps/server/src/api/auth/types.ts
export type EntityLevelPermission = Readonly<{
specificEntities: ReadonlySet<string>, // Entity IDs
entitiesFromCollection: ReadonlySet<string>, // Collection IDs
}> | '*'

When a user requests an access token, the server consults their role permissions and sets them accordingly. Additionally, if the browser has active share tokens, the granted permissions are combined from both the user’s role and the share tokens.

Token Types

The server employs three distinct token types:

Refresh Token

Stored in a refresh cookie, this JSON Web Token (JWT) contains the logged-in user’s ID and role. It is called a “refresh token” because it’s used to obtain a new access token. The server maintains a record of valid tokens in Valkey. A refresh request via the /api/auth/refresh endpoint triggers token rotation.

Share Token

Users can generate share tokens to grant others (limited) editing access without requiring an account (sharing via URI). Each share token is associated with specific access rights.

Share tokens can be activated by including them in a POST request to the /api/auth/refresh endpoint, adding them to the shares cookie (a comma-separated list of share tokens).

Access Token

Stored in an access cookie, this JWT contains the current access rights. It’s set by sending a request to the /api/auth/refresh endpoint. Access rights are determined by combining those from the logged-in user and active share tokens.

Unlike refresh tokens, access tokens have a short validity period and must be refreshed once they expire. They are used to verify permissions for actions requiring authorization (implementation pending). This approach avoids frequent permission checks for every request, streamlining server performance.

Sign-In Flow

  1. The Dashboard redirects to /api/auth/signin.
  2. Users select a login provider (or are automatically redirected if only one is available) and are taken to /api/auth/signin/start/<providerId>.
  3. Users complete the OAuth/OpenID flow at the chosen provider, which redirects them to /api/auth/complete/<providerId> with the authentication token in the URI parameter.
  4. The server retrieves the user’s ID from the login provider and checks for an associated account:
    • If an account exists, the user is logged in by setting the refresh cookie.
    • If no account exists, a new account is created, and the user is logged in with the refresh cookie.
  5. The user is redirected back to the Dashboard.

Login Providers

Currently we only support Discord OAuth2 as a login provider type. We plan to expand the list to a handfull of big names like Google and we also want to support generic OpenID.

To test or develop with Discord you need to register a developer application. In the developer application go to the OAuth2 Section and add the following redirects:

https://localhost:3000/api/auth/signin/complete/<ID>

and

http://localhost:3000/api/auth/signin/complete/<ID>

Make sure to set the port to the one your dev. server runs on (not the Dashboard’s port) and replace <ID> with any string of characters and numbers for example discord.

The next step is to register the login provider in the Server. Since the Server supports multiple login providers, they are configured through a JSON Array in the following format:

[{
"type": "<PROVIDER_TYPE>",
"id": "<ID>",
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>"
}]

Use the template and replace the placeholders with the correct values. The only supported type is DiscordOAuthProvider. Set the ID to the same one you set in the step before. The client ID and secret can be accessed on the OAuth2 page of the Discord application you registered.

Then strip all newlines from the template and set the LOGIN_PROVIDERS environment variable in the apps/server/.env.local file. It will look something like this:

export LOGIN_PROVIDERS='[{"type": "DiscordOAuthProvider","id": "discord","client_id":"HIDDEN","client_secret":"HIDDEN"}]'

Don’t forget to surrount the JSON String with single quotes (') and do not use any within the String.