feat(auth): invite links for several people, invite by email, registration with approval or email domains
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s

- Invites: link /?invite=CODE, copy link, send by email, max uses with counter
- Registration modes closed | approval | domain | domain+approval | open, waiting list with approve/reject
- REGISTRATION_MODE / REGISTRATION_DOMAINS lock the admin setting; ALLOW_REGISTRATION=true is honoured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 13:43:54 +01:00
1 parent db5c5cde9b
commit af77b9e809
24 files changed
+1077 -142

No files matched your search

+13 -3
View File
@@ -271,12 +271,13 @@ export const api = {
password: string,
displayName: string,
inviteCode?: string
): Promise<{ token: string; user: User }> {
const res = await request<{ token: string; user: User }>('/api/v1/auth/register', {
): Promise<{ token?: string; user?: User; pending?: boolean; message?: string }> {
const res = await request<{ token?: string; user?: User; pending?: boolean; message?: string }>('/api/v1/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, displayName, inviteCode: inviteCode || '' }),
});
setToken(res.token);
// Waiting for approval: no session yet
if (res.token) setToken(res.token);
return res;
},
@@ -1121,6 +1122,7 @@ export const api = {
email?: string;
role: string;
organizationId?: string;
maxUses?: number;
}): Promise<UserInvite> {
return request<UserInvite>('/api/v1/admin/invites', {
method: 'POST',
@@ -1128,6 +1130,14 @@ export const api = {
});
},
async adminSendInvite(id: string, email: string): Promise<UserInvite> {
return request<UserInvite>(`/api/v1/admin/invites/${id}/send`, { method: 'POST', body: JSON.stringify({ email }) });
},
async adminApproveUser(id: string): Promise<User> {
return request<User>(`/api/v1/admin/users/${id}/approve`, { method: 'POST' });
},
async adminDeleteInvite(id: string): Promise<void> {
await request(`/api/v1/admin/invites/${id}`, { method: 'DELETE' });
},
+14
View File
@@ -2,9 +2,14 @@ export type ThoughtStatus = 'inbox' | 'kept' | 'discarded' | 'converted';
export type PriorityLevel = 'low' | 'normal' | 'high' | 'urgent';
export type UserRole = 'admin' | 'member';
export type RegistrationMode = 'closed' | 'approval' | 'domain' | 'domain+approval' | 'open';
export interface AuthStatus {
needsSetup: boolean;
allowRegistration: boolean;
// Registering without an invite and the e-mail domains of the domain modes (hint on the form)
registrationMode?: RegistrationMode;
registrationDomains?: string[];
instanceName: string;
demoMode?: boolean;
sharingMode?: SharingMode;
@@ -94,6 +99,8 @@ export interface User {
avatarUrl?: string;
role: UserRole;
isActive: boolean;
// Registered and waiting for an admin's approval (admin user list)
pendingApproval?: boolean;
alarmChannels?: string[];
alarmEmail?: string;
discordWebhookUrl?: string;
@@ -115,6 +122,9 @@ export interface UserInvite {
usedBy?: string;
createdAt: string;
expiresAt: string;
// A link for several people: allowed and used redemptions
maxUses: number;
useCount: number;
}
export interface SubTask {
@@ -250,6 +260,10 @@ export interface SystemSettings {
smtpSkipVerify?: boolean;
allowPrivateWebhooks?: boolean; // alarm webhooks may reach the LAN (and plain http)
smtpConfiguredViaEnv?: boolean;
registrationMode?: RegistrationMode;
registrationDomains?: string;
// REGISTRATION_MODE / REGISTRATION_DOMAINS (or ALLOW_REGISTRATION=true) fix it: shown, not editable
registrationConfiguredViaEnv?: boolean;
// Automatic backups: daily time (HH:MM, server time zone) and days to keep them
backupEnabled?: boolean;
backupTime?: string;