Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 119 additions & 31 deletions apps/web/content/docs/dev/events/built-in-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,29 @@ core event as for one of your own.

## Core events (`@vitnode/core`)

Fourteen names, all declared in `VitNodeEvents` in
Seventeen names, all declared in `VitNodeEvents` in
`packages/vitnode/src/api/models/events.ts`. Every one of them fires **after**
the write it describes has committed.

| Event | Payload | Fires when |
| ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `user.created` | `{ userId, email, name, emailVerified }` | A user row is inserted - public sign-up, AdminCP creation, or SSO sign-up |
| `user.updated` | `{ userId, email, name }` | A user is edited in the AdminCP, or a member saves their own profile or time zone in **Settings → Overview** |
| `user.deleted` | `{ userId, email }` | Never - the name is declared for plugins, core has no deletion flow |
| `user.sso.linked` | `{ userId, email, providerId }` | An SSO identity is linked to an existing account - at login with its password, or from **Settings → Connected accounts** |
| `user.sso.unlinked` | `{ userId, providerId }` | A member disconnects a provider in **Settings → Connected accounts** |
| `user.sso.profile_synced` | `{ userId, providerId, fields, trigger }` | Profile fields were copied from a provider - by **Sync now**, **Import** or an opted-in sign-in |
| `user.passkey.created` | `{ userId, passkeyId }` | A member adds a [passkey](/docs/dev/passkeys) in **Settings → Security** |
| `user.passkey.updated` | `{ userId, passkeyId, name }` | A member renames one of their passkeys |
| `user.passkey.deleted` | `{ userId, passkeyId }` | A member removes one of their passkeys |
| `user.avatar.updated` | `{ userId, fileId }` | An avatar is uploaded or removed - by the user on their profile, or by staff in the AdminCP |
| `user.cover.updated` | `{ userId, fileId }` | A profile cover is uploaded or removed - by the user on their profile, or by staff in the AdminCP |
| `role.created` | `{ roleId }` | A role is created in the AdminCP |
| `role.updated` | `{ roleId }` | A role is edited in the AdminCP |
| `role.deleted` | `{ roleId }` | A role is deleted in the AdminCP |
| Event | Payload | Fires when |
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `user.created` | `{ userId, email, name, emailVerified }` | A user row is inserted - public sign-up, AdminCP creation, or SSO sign-up |
| `user.updated` | `{ userId, email, name }` | A user is edited in the AdminCP, or a member saves their own profile or time zone in **Settings → Overview** |
| `user.deleted` | `{ userId, email }` | Never - the name is declared for plugins, core has no deletion flow |
| `user.password.updated` | `{ userId }` | A password is set - by a member completing a reset, or by staff on the AdminCP user page |
| `user.sessions.revoked` | `{ userId, deviceId, sessions }` | A user is signed out remotely - one device or all of them, by the member or by staff, or after a password change |
| `user.sso.linked` | `{ userId, email, providerId }` | An SSO identity is linked to an existing account - at login with its password, or from **Settings → Connected accounts** |
| `user.sso.unlinked` | `{ userId, providerId }` | A provider is disconnected - by the member in **Settings → Connected accounts**, or by staff in the AdminCP |
| `user.sso.preferences_updated` | `{ userId, sources, sync }` | Profile-field sources or sync-on-sign-in settings are saved - by the member or by staff |
| `user.sso.profile_synced` | `{ userId, providerId, fields, trigger }` | Profile fields were copied from a provider - by **Sync now**, **Import** or an opted-in sign-in |
| `user.passkey.created` | `{ userId, passkeyId }` | A member adds a [passkey](/docs/dev/passkeys) in **Settings → Security** |
| `user.passkey.updated` | `{ userId, passkeyId, name }` | A passkey is renamed - by its owner or by staff |
| `user.passkey.deleted` | `{ userId, passkeyId }` | A passkey is removed - by its owner or by staff |
| `user.avatar.updated` | `{ userId, fileId }` | An avatar is uploaded or removed - by the user on their profile, or by staff in the AdminCP |
| `user.cover.updated` | `{ userId, fileId }` | A profile cover is uploaded or removed - by the user on their profile, or by staff in the AdminCP |
| `role.created` | `{ roleId }` | A role is created in the AdminCP |
| `role.updated` | `{ roleId }` | A role is edited in the AdminCP |
| `role.deleted` | `{ roleId }` | A role is deleted in the AdminCP |

<Accordions>
<Accordion title="user.created">
Expand Down Expand Up @@ -267,6 +270,87 @@ avatar that failed to download never counts. A name change also emits

**A listener would** refresh a search index or a cached member card.

</Accordion>
<Accordion title="user.password.updated">

Emitted after a new password hash is stored - when a member completes a
password reset, or when staff set one from the AdminCP user page. The staff
route answers `403` while password sign-in is turned off, so this never fires
for a password nobody could use. Both paths then sign the account out
everywhere, so `user.sessions.revoked` follows right after.

<TypeTable
type={{
userId: {
description: 'Id of the account whose password changed.',
type: 'number',
},
}}
/>

**A listener would** email the account owner that their password changed, and
use the envelope's `actor` to say whether they or an administrator did it.

</Accordion>
<Accordion title="user.sessions.revoked">

Emitted by `revokeSessions` once a user's sessions are deleted and their cached
copies dropped. It covers both session kinds - the site session and the AdminCP
session - and fires from every place that signs somebody out remotely: a member
ending one of their own devices, staff ending one device or all of them, and a
password change.

<TypeTable
type={{
userId: {
description: 'Id of the account that was signed out.',
type: 'number',
},
deviceId: {
description:
'The device that was signed out, or `null` when every device was.',
type: 'number | null',
},
sessions: {
description:
'How many sessions were deleted, site and AdminCP together. Can be `0` when there was nothing left to end.',
type: 'number',
},
}}
/>

**A listener would** write a security audit entry, or warn the member when
staff signed them out.

</Accordion>
<Accordion title="user.sso.preferences_updated">

Emitted by `SsoConnectionModel.savePreferences` after the profile-field sources
and the per-provider sync switches are saved - from **Settings → Connected
accounts** or from the AdminCP user page. It fires on every successful save,
even when nothing changed.

<TypeTable
type={{
userId: {
description: 'Id of the account the settings belong to.',
type: 'number',
},
sources: {
description:
'The submitted source per profile field - a provider id, or `null` for managed manually. Fields left out were not changed.',
type: "Partial<Record<'avatar' | 'firstName' | 'lastName', string | null>>",
},
sync: {
description: 'The submitted sync-on-sign-in switch per provider id.',
type: 'Record<string, boolean>',
},
}}
/>

**A listener would** audit-log who changed where an account's profile comes
from.

</Accordion>
<Accordion title="user.deleted (declared, never emitted)">

Expand Down Expand Up @@ -404,23 +488,27 @@ or the bottom bar, never both.

## Notification events

Seven names, declared in `VitNodeEvents` in
`packages/vitnode/src/api/models/events.ts`. Each fires **after** an
Eight names, declared in `VitNodeEvents` in
`packages/vitnode/src/api/models/events.ts`. Seven of them fire **after** an
administrator's change in [AdminCP → Notifications](/docs/dev/notifications/admincp)
has been saved. The envelope's `actor` is that administrator.

| Event | Payload | Fires when |
| --------------------------------- | -------------------- | -------------------------------------------------------------------------- |
| `notifications.type.updated` | `{ typeId }` | What a type does by default (list, push, email, member can edit) changes |
| `notifications.preferences_reset` | `{ members }` | **Reset all members to defaults** ran - `members` is how many were reset |
| `notifications.paused` | `{}` | Notifications were paused from the danger zone |
| `notifications.resumed` | `{ requeuedEvents }` | Notifications were resumed - `requeuedEvents` waited while paused |
| `notifications.emails_cancelled` | `{ count }` | Queued emails were cancelled - `count` is how many deliveries were skipped |
| `notifications.read_all` | `{ items, members }` | Everything was marked as read for everyone |
| `notifications.deleted_all` | `{ items }` | Every member's notifications were deleted |
has been saved, and the envelope's `actor` is that administrator.
`notifications.preferences_updated` is about one member: it fires when their
own preferences are saved, by them or by staff on their AdminCP user page.

| Event | Payload | Fires when |
| ----------------------------------- | --------------------- | -------------------------------------------------------------------------- |
| `notifications.type.updated` | `{ typeId }` | What a type does by default (list, push, email, member can edit) changes |
| `notifications.preferences_updated` | `{ userId, typeIds }` | One member's preferences are saved - `typeIds` are the types in the save |
| `notifications.preferences_reset` | `{ members }` | **Reset all members to defaults** ran - `members` is how many were reset |
| `notifications.paused` | `{}` | Notifications were paused from the danger zone |
| `notifications.resumed` | `{ requeuedEvents }` | Notifications were resumed - `requeuedEvents` waited while paused |
| `notifications.emails_cancelled` | `{ count }` | Queued emails were cancelled - `count` is how many deliveries were skipped |
| `notifications.read_all` | `{ items, members }` | Everything was marked as read for everyone |
| `notifications.deleted_all` | `{ items }` | Every member's notifications were deleted |

**A listener would** post an audit entry to your staff channel when someone
pauses, deletes or resets everything.
pauses, deletes or resets everything, or note when staff changed how a single
member is notified.

## Page layout events

Expand Down
47 changes: 47 additions & 0 deletions apps/web/content/docs/dev/working-with-users/users.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,53 @@ Administrators can view, edit, ban, and assign roles to users at **Core → User

---

### Edit a user

{/* Image prompt: VitNode AdminCP user page at /admin/core/users/1. Left column: profile card with cover, round avatar, name, @handle, Staff and Verified badges, then Personal information, Preferences, Roles, Connected accounts and Devices cards. Right column: segmented tabs Activity, Notifications, Password & passkeys above a vertical activity timeline. Dark theme, 1440x1000. */}

Open a user at `/admin/core/users/[id]`. Everything the member can change in their own `/settings` is editable here too.

| Area | What staff can change |
| :----------------------- | :------------------------------------------------------------------------------------------------------ |
| Profile card | Display name, name code, email, avatar, cover, and **Mark email as verified** |
| Personal information | First name, last name, headline, phone, birthday, **Show real name** |
| Preferences | Language, time zone (or automatic), newsletter |
| Roles | Primary role and secondary roles |
| Connected accounts | Disconnect an SSO account, choose where the avatar and name come from, turn **Sync on sign-in** on/off |
| Devices | Sign out one device, or every device at once |
| **Notifications** tab | In-app, push and email (with frequency) per notification type |
| **Password & passkeys** tab | Set a new password, rename or delete passkeys |

A few rules differ from the member's own settings:

- **Staff can edit locked fields.** The role's **Allow editing personal information** and the install's `users.personalInformation` switches limit members, not staff.
- **Notification locks still apply.** Mandatory types, and types the notification settings stop members from changing, are read-only here too, because delivery ignores stored choices for them.
- **The last sign-in method can't be removed.** Disconnecting the only SSO account or deleting the only passkey answers `409`.
- **Setting a password signs the member out everywhere**, AdminCP sessions included.
- **Sync now isn't offered.** It needs the member to sign in with the provider themselves.

Every write needs `users:can_edit`, plus `users:can_edit_admin` when the target is staff. Reads need `users:can_view`.

| Method | Path | Does |
| :------- | :------------------------------------------------------------------ | :--------------------------------------------------- |
| `PATCH` | `/api/@vitnode/core/admin/users/{id}` | Account, personal information and preferences |
| `PUT` | `/api/@vitnode/core/admin/users/{id}/password` | Sets a new password and revokes every session |
| `GET` | `/api/@vitnode/core/admin/users/{id}/devices` | Devices with an active session |
| `DELETE` | `/api/@vitnode/core/admin/users/{id}/devices/{publicId}` | Signs one device out |
| `DELETE` | `/api/@vitnode/core/admin/users/{id}/devices` | Signs every device out |
| `GET` | `/api/@vitnode/core/admin/users/{id}/passkeys` | Passkeys |
| `PATCH` | `/api/@vitnode/core/admin/users/{id}/passkeys/{passkeyId}` | Renames a passkey |
| `DELETE` | `/api/@vitnode/core/admin/users/{id}/passkeys/{passkeyId}` | Deletes a passkey |
| `GET` | `/api/@vitnode/core/admin/users/{id}/sso` | Connected accounts, sign-in summary, profile sources |
| `DELETE` | `/api/@vitnode/core/admin/users/{id}/sso/{providerId}` | Disconnects an account |
| `PUT` | `/api/@vitnode/core/admin/users/{id}/sso/preferences` | Profile sources and sync on sign-in |
| `GET` | `/api/@vitnode/core/admin/users/{id}/notification-preferences` | Notification preferences per type |
| `PUT` | `/api/@vitnode/core/admin/users/{id}/notification-preferences` | Changes notification preferences |

The `PATCH` body takes any of `email`, `name`, `nameCode`, `roleId`, `secondaryRoleIds`, `firstName`, `lastName`, `headline`, `phone`, `showRealName`, `birthday` (`YYYY-MM-DD` or `null`), `language`, `timeZone` (`null` for automatic) and `newsletter`. At least one field is required.

---

### Where it happens

- **Profile page** (`/users/[nameCode]`): the owner sees a camera button on the
Expand Down
Loading
Loading