User Profile
Every Oidis application built on the Hub stack comes with a per-user profile system. The User model stores personal information (name, email, phone), security settings (password, 2FA, auth tokens), group memberships, and a profile avatar — all persisted in MongoDB.
The system provides two built-in interfaces for managing profiles:
- Admin interface (
/users) — full user management for administrators - Self-service panel (
/profile) — users manage their own data
Both are designed for extension — end applications add custom fields and UI controls through inheritance.
User Profile is part of io-oidis-hub (backend) and io-oidis-hub-gui (frontend). It is closely related to the Authentication system, which handles login, sessions, and access control.
The User Model
The frontend User model defines all properties available for profile management:
import { InclusionConverter } from "@io-oidis-commons/Io/Oidis/Commons/DAO/Converters/InclusionConverter.js";
import { Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { BaseModel } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/BaseModel.js";
export class User extends BaseModel {
@Property({type: String})
public declare UserName : string;
@Property({type: String, required: false})
public declare Firstname : string;
@Property({type: String, required: false})
public declare Lastname : string;
@Property({type: String, required: false, validator: new EmailValidator()})
public declare Email : string;
@Property({type: String, required: false, validator: new PhoneValidator()})
public declare Phone : string;
@Property({type: [String], default: []})
public declare AltEmails : string[];
@Property({type: [Group], default: []})
public declare Groups : Group[];
@Property({type: Group})
public declare DefaultGroup : Group;
@Property({type: [AuthToken], default: []})
public declare AuthTokens : AuthToken[];
@Property({type: Buffer, required: false})
public declare Image : Buffer;
@Property({type: UserAuditLog})
public declare AuditLog : UserAuditLog;
@Property({type: Boolean, required: false, default: true})
public declare Activated : boolean;
@Property({type: String, required: false})
public declare TwoFASecret : string;
}
Properties
| Property | Type | Purpose |
|---|---|---|
UserName | string | Login identifier (unique, no . or @ characters) |
Firstname / Lastname | string | Personal name (optional, used for avatar initials) |
Email | string | Primary email, validated by EmailValidator |
Phone | string | Phone number, validated by PhoneValidator |
AltEmails | string[] | Alternative email addresses |
Password | string | Hashed password (never sent to frontend in plain text) |
Groups | Group[] | Permission groups the user belongs to |
DefaultGroup | Group | Hidden personal group for individual method permissions |
AuthTokens | AuthToken[] | API authentication tokens with optional expiry |
Image | Buffer | Profile avatar image (see Avatar System) |
SSOTokens | SSOTokens | SSO integration tokens |
AuditLog | UserAuditLog | Login history: LastLogIn (last successful password or SSO sign-in), LastLogInAttempt and LogInAttempts (password attempts driving the singInCheck lockout), activation notices |
Activated | boolean | Whether the account is active |
TwoFASecret | string | TOTP secret for two-factor authentication |
AuthCode | AuthCode | Authentication code (nested, InclusionConverter) |
WhatsNewSeen | string | Last seen release notes version |
Dormant Accounts
AuthManager.FindDormantUsers($days = 180) (admin dashboard → Permissions → "Dormant accounts") lists activated,
non-deleted users whose AuditLog.LastLogIn is older than the threshold, together with every user that never signed
in, however recently it was created. Each record carries NeverLoggedIn; for those users
LastLogIn is null and DaysInactive counts from Created. getDormantAccountEmails($days) returns the e-mails of
the same set. Independently, CheckActivation only reminds, deactivates or removes users with a pending activation
token.
Backend vs Frontend Model
The backend entity (@Entity("user") in io-oidis-hub) uses the same properties but with additional converters for database references:
| Property | Frontend Type | Backend Converter |
|---|---|---|
Image | Buffer | FileEntryToRefConverter — stores as FileEntry reference |
Groups | Group[] | GroupListToRefConverter — stores as DB references |
DefaultGroup | Group | GroupToRefConverter — stores as DB reference |
AuthTokens | AuthToken[] | AuthTokenToRefConverter — stores as DB references |
AltEmails | string[] | ArrayConverter — explicit array handling |
The converters ensure that related entities (groups, tokens, image files) are stored as MongoDB document references on the backend but arrive as fully resolved objects on the frontend.
Avatar System
User avatars use a three-level fallback chain to ensure every user always has a visible profile image:
1. Custom image → User-uploaded JPG/PNG, stored as FileEntry in MongoDB
↓ (not found)
2. Gravatar → Auto-fetched from gravatar.com using MD5(email)
↓ (not found)
3. Generated avatar → CanvasHelper.CreateAvatar() — initials on themed background
Custom Image Upload
Users upload an avatar through the profile panel (JPG or PNG). The image is sent as a base64 string to the backend, which stores it as a FileEntry in MongoDB. On retrieval, the backend converts it back to a base64 data URL.
Gravatar Fallback
When no custom image exists, the backend attempts to fetch the user's Gravatar using an MD5 hash of their email address. The request uses ?d=404 to get a 404 response (instead of a default image) when no Gravatar exists, allowing the system to fall through to the next level.
Generated Initials Avatar
If both custom image and Gravatar are unavailable, the frontend generates an avatar using CanvasHelper.CreateAvatar():
export class CanvasHelper extends BaseObject {
public static CreateAvatar(user : User, $size : number = 256) : string {
let initials = "??";
if (!ObjectValidator.IsEmptyOrNull(user)) {
if (user.Firstname || user.Lastname) {
initials = `${user.Firstname ? user.Firstname.charAt(0) : ""}${user.Lastname ? user.Lastname.charAt(0) : ""}`;
if (initials.length === 1) {
initials = (user.Firstname || user.Lastname).substring(0, 2);
}
} else if (user.UserName) {
initials = user.UserName.substring(0, 2);
} else if (user.Email) {
initials = user.Email.substring(0, 2);
}
}
// Renders uppercase initials on a canvas using Bootstrap theme colors:
// Background: --bs-primary-bg-subtle (#f2dbe7)
// Text: --bs-primary (#bd4987)
// Font: Application's --bs-font-sans-serif
return canvas.toDataURL("image/png");
}
}
Initials priority: Firstname + Lastname → UserName → Email → ??
The generated avatar respects the application's Bootstrap theme — if the app customizes --bs-primary, the avatar colors change automatically.
Display Pattern
Both the admin panel and the self-service profile use the same pattern for displaying avatars:
instance.profileImg.Fallback(CanvasHelper.CreateAvatar(profile)); // Canvas fallback
instance.profileImg.Source(<any>profile.Image); // Custom/Gravatar image
The Fallback() method sets the image shown when Source() has no data. This means the canvas avatar is always ready as a fallback, and the custom image (or Gravatar data URL from the backend) overrides it when available.
Admin Interface — UsersPanelController
The admin interface provides full user management capabilities at the /users route.
@Resolver("/users", ["/users/{userId}"])
export class UsersPanelController<Panel extends UsersPanel> extends BasePanelController<Panel> {
protected currentProfile : User;
protected localization : IUsersPanelLocalization;
private auth : AuthManagerConnector;
private persistence : IPersistenceHandler;
constructor($instance : Panel) {
super($instance);
this.auth = AuthManagerConnector.getClient();
this.persistence = PersistenceFactory.getPersistence(this.getClassName());
}
}
Access: Admin and SysAdmin roles only.
Features
| Feature | Description |
|---|---|
| User search | Autocomplete search via UserSearchBinding — find any user by name/email |
| Profile editing | Name, email, phone, avatar upload (JPG/PNG via FileUploadBinding) |
| Password management | Change password, generate random password, create reset/invite links (copyable or sent via email) |
| Groups & permissions | Add/remove group memberships, manage individual method permissions on DefaultGroup |
| Auth tokens | Generate, rename, set expiry, reveal (with password verification for non-admins), revoke |
| Two-factor auth | Generate 2FA secret, display QR code (BarCodeEngine), activate/deactivate |
| Account lifecycle | Activate, deactivate, restore deleted, permanently delete |
| Permissions export | Export user's authorized methods as JSON file |
| Session persistence | Remembers last-viewed profile via PersistenceFactory |
Key Methods
FetchUserProfile($name)— loads a user profile from the backend, populates all panel controls, renders groups/tokens/2FA stateFetchData()— entry point: resolves userId from URL params or persistence, callsFetchUserProfileresolver()— binds all button click handlers (save, reset, password, tokens, 2FA, admin actions)processProfileData($profile)— populates basic profile fields (name, email, phone) — override point for extensionsgetUserProfileData()— reads profile fields from panel controls intoIUserProfileOptions— override point for extensions
Localization
The controller uses IUsersPanelLocalization for UI messages (error toasts, button labels). Override this in your extended controller to localize messages.
Self-Service — MyProfilePanelController
The self-service profile panel is available to all authenticated users at the /profile route. It uses this.context (UserContext) to check authentication and retrieve the current user's ID:
@Resolver("/profile")
export class MyProfilePanelController<Panel extends MyProfilePanel> extends UsersPanelController<Panel> {
public async AccessValidator() : Promise<boolean> {
return this.context.IsAuthenticated;
}
public async FetchData() : Promise<void> {
await this.FetchUserProfile(this.context.getUserProfile().Id);
}
}
Disabled Admin Features
MyProfilePanel extends UsersPanel and disables admin-only features in its constructor:
export class MyProfilePanel extends UsersPanel {
public readonly firstName : TextField;
public readonly lastName : TextField;
constructor() {
super();
this.firstName = new TextField();
this.lastName = new TextField();
this.profileSearch.Enabled(false); // No user search
this.groups.Enabled(false); // No group management
this.profileTokens.Enabled(false); // No token management
}
}
Added Fields
The self-service panel adds firstName and lastName fields not present in the admin panel. The controller overrides handle these:
protected processProfileData($profile : User) : void {
super.processProfileData($profile);
const instance : MyProfilePanel = this.getInstanceOwner();
instance.firstName.Value($profile.Firstname);
instance.lastName.Value($profile.Lastname);
}
protected getUserProfileData() : IUserProfileOptions {
const instance : MyProfilePanel = this.getInstanceOwner();
return JsonUtils.Extend(super.getUserProfileData(), {
firstname: instance.firstName.Value(),
lastname : instance.lastName.Value()
});
}
Hub GUI's MyProfilePanel ships with Czech labels by default ("Jméno", "Příjmení", "Telefon"). Override getProfile() in your extended panel to localize labels for your application's target language.
Extending the User Profile
End applications commonly extend the user profile with custom fields. The system supports this through inheritance at every layer — the same pattern used for Application Settings.
Step 1 — Extend the Backend User Entity
Add custom @Property fields to the backend User model:
import { Entity, Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { User as HubUser } from "@io-oidis-hub/Io/Oidis/Hub/DAO/Models/User.js";
@Entity("user")
export class User extends HubUser {
@Property({type: String, required: false})
public declare Department : string;
@Property({type: String, required: false})
public declare PreferredTheme : string;
}
The extended entity must use the same @Entity("user") collection name as the base entity.
Step 2 — Extend the Frontend User Model
Mirror the custom properties in the frontend model:
import { Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { User as HubUser } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Models/User.js";
export class User extends HubUser {
@Property({type: String, required: false})
public declare Department : string;
@Property({type: String, required: false})
public declare PreferredTheme : string;
}
Step 3 — Extend the Profile Panel
Add custom UI controls by extending MyProfilePanel and overriding getProfile():
import { MyProfilePanel as HubPanel } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Panels/MyProfilePanel.js";
import { TextField } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/TextField.js";
import { DropDown } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/DropDown.js";
export class MyProfilePanel extends HubPanel {
public readonly department : TextField;
public readonly preferredTheme : DropDown;
constructor() {
super();
this.department = new TextField();
this.preferredTheme = new DropDown();
}
protected getProfile() : string {
return `
${super.getProfile()}
<div class="input-group" style="margin-top: 5px;">
<span class="input-group-text" style="width: 20%;">Department</span>
<input class="form-control" type="text" data-oidis-bind="${this.department}" />
</div>
<div class="input-group" style="margin-top: 5px;">
<span class="input-group-text" style="width: 20%;">Theme</span>
<select class="form-select" data-oidis-bind="${this.preferredTheme}"></select>
</div>`;
}
}
Step 4 — Extend the Controller
Override processProfileData and getUserProfileData to handle the custom fields:
import { Resolver } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { JsonUtils } from "@io-oidis-commons/Io/Oidis/Commons/Utils/JsonUtils.js";
import { IUserProfileOptions } from "@io-oidis-services/Io/Oidis/Services/Connectors/AuthManagerConnector.js";
import { MyProfilePanelController as HubController } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Controllers/Panels/MyProfilePanelController.js";
import { User } from "../../Models/User.js";
import { MyProfilePanel } from "../../Panels/MyProfilePanel.js";
@Resolver("/profile")
export class MyProfilePanelController extends HubController<MyProfilePanel> {
protected processProfileData($profile : User) : void {
super.processProfileData($profile);
const instance : MyProfilePanel = this.getInstanceOwner();
instance.department.Value($profile.Department);
instance.preferredTheme.Value($profile.PreferredTheme);
}
protected getUserProfileData() : IUserProfileOptions {
const instance : MyProfilePanel = this.getInstanceOwner();
return JsonUtils.Extend(super.getUserProfileData(), {
department : instance.department.Value(),
preferredTheme: instance.preferredTheme.Value()
});
}
}
Both UsersPanelController and MyProfilePanelController use the generic type parameter <Panel extends ...>, specifically designed for this extension pattern. Your extended controller gets full type safety for the custom panel controls.
Step 5 — Map the Options on the Backend
setUserProfile() and Register() copy only the Hub fields (name, e-mail, phone, activation, 2FA, image) from the options to the entity. Custom fields need an override of processUserData() in the application AuthManager — an option that is not mapped there is silently dropped, although the panel sends it:
import { ObjectValidator } from "@io-oidis-commons/Io/Oidis/Commons/Utils/ObjectValidator.js";
import { UsersManager as HubUsersManager } from "@io-oidis-hub/Io/Oidis/Hub/AuthProcessor/UsersManager.js";
import { AuthManager as Parent } from "@io-oidis-hub/Io/Oidis/Hub/Utils/AuthManager.js";
import { IUserProfileOptions } from "@io-oidis-services/Io/Oidis/Services/Connectors/AuthManagerConnector.js";
import { User } from "../Models/User.js";
export abstract class UsersManager extends HubUsersManager {
protected getUserInstance(this : AuthManager) : User {
return new User();
}
protected async processUserData(this : AuthManager, $user : User, $options : IProfileOptions) : Promise<void> {
await super.processUserData.apply(this, [$user, $options]);
if (ObjectValidator.IsSet($options.department)) {
$user.Department = $options.department;
}
if (ObjectValidator.IsSet($options.preferredTheme)) {
$user.PreferredTheme = $options.preferredTheme;
}
}
}
export class AuthManager extends Parent {
// @ts-expect-error: override of partial protected API
protected getUserInstance = UsersManager.prototype.getUserInstance;
// @ts-expect-error: override of partial protected API
protected processUserData = UsersManager.prototype.processUserData;
}
export interface IProfileOptions extends IUserProfileOptions {
department? : string;
preferredTheme? : string;
}
Creating Users on the Server
Seeds, imports and admin tools create accounts through the backend AuthManager (Loader.getInstance().getAuthManager()), the same way the registration form does:
Register({email, name, password, withActivation: false, withInvitation: false, throwExceptions: true, ...})creates the account with its hidden default group and sends no e-mail. The password option is hashed — pass a random value for accounts that must not sign in.Register()returnsboolean, not the user — resolve the new account by its e-mail afterwards.Activatedis set fromAppSettings.AutoActivateRegisteredafter the options are applied, so anactivatedoption has no effect there. SetActivatedon the loaded user and save it again.RemoveUser(id)deletes the account for good, together with its default group and tokens;DeleteUser(id)only deactivates and soft-deletes it.- Without
strictEmailCheck, e-mail lookups — including the duplicate check ofRegister()— ignore punctuation (.,-,_, …) and letter case and match a part of the stored address.
User Profile vs ApplicationSettings
| Aspect | User Profile | ApplicationSettings |
|---|---|---|
| Scope | Per-user | App-wide (all users) |
| Storage | MongoDB user collection | MongoDB appSettings collection |
| Access | Self-service (/profile) + Admin (/users) | Admin-only panel |
| Use case | Personal data, avatar, preferences, 2FA | Mail engine, default permissions, feature flags |
| JSONP sync | No — created at registration | Yes — initialized from commons.jsonp |
| Extensible | Yes — entity + panel + controller + processUserData() | Yes — entity + panel + controller |
For app-wide settings that all users share, use Application Settings. For per-user preferences and personal data, use the User Profile system.
Quick Reference
| What | Location | Package |
|---|---|---|
| User model (FE) | Hub/Gui/Models/User.ts | io-oidis-hub-gui |
| User entity (BE) | Hub/DAO/Models/User.ts | io-oidis-hub |
| UsersPanelController | Hub/Gui/Controllers/Panels/UsersPanelController.ts | io-oidis-hub-gui |
| MyProfilePanelController | Hub/Gui/Controllers/Panels/MyProfilePanelController.ts | io-oidis-hub-gui |
| UsersPanel | Hub/Gui/Panels/Dashboard/UsersPanel.ts | io-oidis-hub-gui |
| MyProfilePanel | Hub/Gui/Panels/MyProfilePanel.ts | io-oidis-hub-gui |
| CanvasHelper (avatar) | Hub/Gui/Utils/CanvasHelper.ts | io-oidis-hub-gui |
| UsersManager (BE CRUD) | Hub/AuthProcessor/UsersManager.ts | io-oidis-hub |
| AuthManagerConnector (FE) | Hub/Gui/Connectors/AuthManagerConnector.ts | io-oidis-hub-gui |
See also: Authentication | Application Settings | Panels and Controllers | Entity and Property | Events System