feat: add multi-user support and role-based access control #31

This commit is contained in:
rustmailer
2025-12-26 14:27:04 +08:00
parent 1e2f526a07
commit 4af5176b65
181 changed files with 23745 additions and 3187 deletions
-64
View File
@@ -1,64 +0,0 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import axiosInstance from "@/api/axiosInstance";
import { AccessToken } from "@/features/access-tokens/data/schema";
export const login = async (password: string) => {
const response = await axiosInstance.post(`/api/login`, password, {
headers: {
"Content-Type": "text/plain",
},
});
return response.data;
};
export const reset_root_token = async () => {
const response = await axiosInstance.post("/api/v1/reset-root-token");
return response.data;
};
export const reset_root_password = async (password: string) => {
const response = await axiosInstance.post("/api/v1/reset-root-password", password, {
headers: {
"Content-Type": "text/plain",
},
});
return response.data;
};
export const list_access_tokens = async () => {
const response = await axiosInstance.get<AccessToken[]>("/api/v1/access-token-list");
return response.data;
};
export const create_access_token = async (data: Record<string, any>) => {
const response = await axiosInstance.post("/api/v1/access-token", data);
return response.data;
}
export const update_access_token = async (token: string, data: Record<string, any>) => {
const response = await axiosInstance.post(`/api/v1/access-token/${token}`, data);
return response.data;
}
export const delete_access_token = async (token: string) => {
const response = await axiosInstance.delete(`/api/v1/access-token/${token}`);
return response.data;
}
+5
View File
@@ -103,3 +103,8 @@ export const autoconfig = async (email: string) => {
const response = await axiosInstance.get<AutoConfigResult>(`/api/v1/autoconfig/${email}`);
return response.data;
};
export const access_assign = async (data: Record<string, any>) => {
const response = await axiosInstance.post("/api/v1/accounts/access/assignments", data);
return response.data;
};
+4 -4
View File
@@ -17,7 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { getAccessToken } from "@/stores/authStore";
import { getToken } from "@/stores/authStore";
import axios from "axios";
// Create an Axios instance
@@ -36,9 +36,9 @@ const axiosInstance = axios.create({
// Add a request interceptor to include the access token in headers
axiosInstance.interceptors.request.use(
(config) => {
const accessToken = getAccessToken(); // Retrieve access token from localStorage
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
const stored = getToken(); // Retrieve access token from localStorage
if (stored) {
config.headers.Authorization = `Bearer ${stored.accessToken}`;
}
return config;
},
+34 -1
View File
@@ -18,7 +18,6 @@
import axiosInstance from "@/api/axiosInstance";
import { Proxy } from "@/features/settings/proxy/data/schema";
export interface Release {
tag_name: string;
@@ -73,6 +72,34 @@ export interface LargestEmail {
size_bytes: number; // Email size in bytes
}
export interface Proxy {
id: number;
url: string;
created_at: number;
updated_at: number;
}
export type ServerConfigurations = {
bichon_log_level: string
bichon_http_port: number
bichon_bind_ip?: string | null
bichon_public_url: string
bichon_cors_origins?: string[] | null
bichon_cors_max_age: number
bichon_ansi_logs: boolean
bichon_log_to_file: boolean
bichon_json_logs: boolean
bichon_max_server_log_files: number
bichon_encrypt_password_set: boolean
bichon_webui_token_expiration_hours: number
bichon_root_dir: string
bichon_metadata_cache_size?: number | null
bichon_envelope_cache_size?: number | null
bichon_enable_rest_https: boolean
bichon_http_compression_enabled: boolean
bichon_sync_concurrency?: number | null
}
export const get_dashboard_stats = async () => {
const response = await axiosInstance.get<DashboardStats>(`/api/v1/dashboard-stats`);
return response.data;
@@ -104,4 +131,10 @@ export const add_proxy = async (url: string) => {
},
});
return response.data;
};
export const get_system_configurations = async () => {
const response = await axiosInstance.get<ServerConfigurations>(`/api/v1/system-configurations`);
return response.data;
};
+202
View File
@@ -0,0 +1,202 @@
import axiosInstance from "@/api/axiosInstance";
export type RoleType = 'Global' | 'Account';
export interface UserRole {
id: number;
name: string;
description?: string | null;
permissions: string[];
is_builtin: boolean;
role_type: RoleType;
created_at: number;
updated_at: number;
}
export function getPermissions(t: (key: string) => string) {
return [
// 1. Global Management
{ label: t('permission.system.access'), value: 'system:access' },
{ label: t('permission.system.root'), value: 'system:root' },
{ label: t('permission.user.manage'), value: 'user:manage' },
{ label: t('permission.user.view'), value: 'user:view' },
{ label: t('permission.token.manage'), value: 'token:manage' },
{ label: t('permission.account.create'), value: 'account:create' },
// 2. Global "ALL" Scoped (Admin)
{ label: t('permission.account.manage_all'), value: 'account:manage:all' },
{ label: t('permission.data.read_all'), value: 'data:read:all' },
{ label: t('permission.data.manage_all'), value: 'data:manage:all' },
{ label: t('permission.data.raw_download_all'), value: 'data:raw:download:all' },
{ label: t('permission.data.delete_all'), value: 'data:delete:all' },
{ label: t('permission.data.export_batch_all'), value: 'data:export:batch:all' },
// 3. Scoped / Limited
{ label: t('permission.account.manage'), value: 'account:manage' },
{ label: t('permission.account.read_details'), value: 'account:read_details' },
{ label: t('permission.data.read'), value: 'data:read' },
{ label: t('permission.data.manage'), value: 'data:manage' },
{ label: t('permission.data.raw_download'), value: 'data:raw:download' },
{ label: t('permission.data.delete'), value: 'data:delete' },
{ label: t('permission.data.export_batch'), value: 'data:export:batch' },
{ label: t('permission.data.import_batch'), value: 'data:import:batch' },
]
}
export interface RateLimit {
quota: number;
interval: number;
}
export interface AccessControl {
ip_whitelist?: string[];
rate_limit?: RateLimit;
}
export type TokenType = "WebUI" | "Api";
export interface AccessToken {
user_id: number;
user_name: string,
user_email: string,
token: string;
created_at: number;
updated_at: number;
name?: string;
last_access_at: number;
expire_at?: number | null;
token_type: TokenType;
}
export interface User {
id: number;
username: string;
email: string;
password?: string | null;
description?: string | null;
global_roles: number[];
global_roles_names: string[];
avatar?: string;
acl?: AccessControl;
account_access_map: Record<number, number>;
account_roles_summary: Record<number, string>;
global_permissions: string[]
account_permissions: Record<number, string[]>
created_at: number;
updated_at: number;
}
export interface LoginResult {
success: boolean;
error_message?: string | null;
access_token?: string | null;
}
export interface MinimalUser {
id: number;
username: string;
email: string;
}
export const login = async (data: Record<string, any>) => {
const response = await axiosInstance.post<LoginResult>(`/api/login`, data);
return response.data;
};
export const reset_admin_token = async () => {
const response = await axiosInstance.post("/api/v1/reset-admin-token");
return response.data;
};
export const reset_admin_password = async (password: string) => {
const response = await axiosInstance.post("/api/v1/reset-admin-password", password, {
headers: {
"Content-Type": "text/plain",
},
});
return response.data;
};
export const list_access_tokens = async () => {
const response = await axiosInstance.get<AccessToken[]>("/api/v1/access-token-list");
return response.data;
};
export const create_access_token = async (data: Record<string, any>) => {
const response = await axiosInstance.post("/api/v1/access-token", data);
return response.data;
}
export const update_access_token = async (token: string, data: Record<string, any>) => {
const response = await axiosInstance.post(`/api/v1/access-token/${token}`, data);
return response.data;
}
export const remove_access_token = async (token: string) => {
const response = await axiosInstance.delete(`/api/v1/access-token/${token}`);
return response.data;
}
export const list_roles = async () => {
const response = await axiosInstance.get<UserRole[]>("/api/v1/list-roles");
return response.data;
};
export const remove_role = async (id: number) => {
const response = await axiosInstance.delete(`/api/v1/roles/${id}`);
return response.data;
};
export const create_role = async (data: Record<string, any>) => {
const response = await axiosInstance.post("/api/v1/roles", data);
return response.data;
};
export const update_role = async (id: number, data: Record<string, any>) => {
const response = await axiosInstance.post(`/api/v1/roles/${id}`, data);
return response.data;
};
export const list_users = async () => {
const response = await axiosInstance.get<User[]>("/api/v1/list-users");
return response.data;
};
export const list_minimal_users = async () => {
const response = await axiosInstance.get<MinimalUser[]>("/api/v1/minimal-user-list");
return response.data;
};
export const remove_user = async (id: number) => {
const response = await axiosInstance.delete(`/api/v1/users/${id}`);
return response.data;
};
export const create_user = async (data: Record<string, any>) => {
const response = await axiosInstance.post("/api/v1/users", data);
return response.data;
};
export const update_user = async (id: number, data: Record<string, any>) => {
const response = await axiosInstance.post(`/api/v1/users/${id}`, data);
return response.data;
};
export const get_user_tokens = async (id: number) => {
const response = await axiosInstance.get<AccessToken[]>(`/api/v1/user-tokens/${id}`);
return response.data;
};
export const get_current_user = async () => {
const response = await axiosInstance.get<User>("/api/v1/current-user");
return response.data;
};