feat: admin can create users manually + disable registration (#85)
Backend:
- POST /api/admin/users — admin-only user creation endpoint
- username & password required, email optional (auto-generated placeholder)
- role, quota_bytes, active all configurable
- creates personal folder automatically
- PUT /api/admin/users/{id}/password — admin password reset
- GET/PUT /api/admin/settings/registration — toggle public registration
- Supports env var OXICLOUD_DISABLE_REGISTRATION override
- Blocks POST /api/auth/register when disabled
- AdminCreateUserDto, AdminResetPasswordDto added to settings DTOs
- registration_enabled field added to DashboardStatsDto
Frontend (admin.html):
- 'Create User' button in Users tab with full modal form
(username, password, email, role, quota)
- 'Reset Password' button per user in actions column
- 'Allow public self-registration' toggle in Dashboard > System
with warning banner when disabled
Closes #85
This commit is contained in:
Generated
+1
-1
@@ -1686,7 +1686,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oxicloud"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -80,6 +80,27 @@ pub struct UpdateUserQuotaDto {
|
||||
pub quota_bytes: i64,
|
||||
}
|
||||
|
||||
/// Request body for admin-created users
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AdminCreateUserDto {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
/// Optional — if omitted, a placeholder email is generated
|
||||
pub email: Option<String>,
|
||||
/// "admin" or "user"; defaults to "user"
|
||||
pub role: Option<String>,
|
||||
/// Storage quota in bytes; 0 = unlimited. If omitted, uses role default.
|
||||
pub quota_bytes: Option<i64>,
|
||||
/// Whether the account is active; defaults to true
|
||||
pub active: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request body for admin password reset
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AdminResetPasswordDto {
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
/// Query parameters for listing users
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListUsersQueryDto {
|
||||
@@ -105,4 +126,5 @@ pub struct DashboardStatsDto {
|
||||
pub storage_usage_percent: f64,
|
||||
pub users_over_80_percent: i64,
|
||||
pub users_over_quota: i64,
|
||||
pub registration_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -267,4 +267,37 @@ impl AdminSettingsService {
|
||||
provider_name_suggestion: suggestion,
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Registration Control
|
||||
// ========================================================================
|
||||
|
||||
/// Check if public self-registration is enabled.
|
||||
/// Priority: env var `OXICLOUD_DISABLE_REGISTRATION` > DB setting > default (true).
|
||||
pub async fn get_registration_enabled(&self) -> bool {
|
||||
// Env var override takes priority
|
||||
if let Ok(val) = std::env::var("OXICLOUD_DISABLE_REGISTRATION") {
|
||||
return !matches!(val.to_lowercase().as_str(), "true" | "1" | "yes");
|
||||
}
|
||||
// Check DB setting
|
||||
match self.settings_repo.get("registration_enabled").await {
|
||||
Ok(Some(val)) => val == "true",
|
||||
_ => true, // default: enabled
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable or disable public self-registration.
|
||||
pub async fn set_registration_enabled(
|
||||
&self,
|
||||
enabled: bool,
|
||||
updated_by: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.settings_repo.set(
|
||||
"registration_enabled",
|
||||
if enabled { "true" } else { "false" },
|
||||
"general",
|
||||
false,
|
||||
Some(updated_by),
|
||||
).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,6 +602,124 @@ impl AuthApplicationService {
|
||||
// Admin User Management Methods
|
||||
// ========================================================================
|
||||
|
||||
/// Admin-only: create a user bypassing registration guards.
|
||||
pub async fn admin_create_user(
|
||||
&self,
|
||||
dto: crate::application::dtos::settings_dto::AdminCreateUserDto,
|
||||
) -> Result<UserDto, DomainError> {
|
||||
// Validate username length
|
||||
if dto.username.len() < 3 || dto.username.len() > 32 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
"Username must be between 3 and 32 characters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check for duplicate username
|
||||
if self.user_storage.get_user_by_username(&dto.username).await.is_ok() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists, "User",
|
||||
format!("User '{}' already exists", dto.username),
|
||||
));
|
||||
}
|
||||
|
||||
// Email: use provided or generate placeholder
|
||||
let email = dto.email
|
||||
.filter(|e| !e.trim().is_empty())
|
||||
.unwrap_or_else(|| format!("{}@oxicloud.local", dto.username));
|
||||
|
||||
// Check email uniqueness
|
||||
if self.user_storage.get_user_by_email(&email).await.is_ok() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AlreadyExists, "User",
|
||||
format!("Email '{}' is already registered", email),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if dto.password.len() < 8 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
"Password must be at least 8 characters long".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Determine role
|
||||
let role = match dto.role.as_deref() {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
// Determine quota
|
||||
let quota = dto.quota_bytes.unwrap_or_else(|| {
|
||||
if role == UserRole::Admin { 107_374_182_400 } else { 1_073_741_824 }
|
||||
});
|
||||
|
||||
// Hash password
|
||||
let password_hash = self.password_hasher.hash_password(&dto.password)?;
|
||||
|
||||
// Create domain entity
|
||||
let user = User::new(
|
||||
dto.username.clone(),
|
||||
email,
|
||||
password_hash,
|
||||
role,
|
||||
quota,
|
||||
).map_err(|e| DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
format!("Error creating user: {}", e),
|
||||
))?;
|
||||
|
||||
// Persist
|
||||
let created = self.user_storage.create_user(user).await?;
|
||||
|
||||
// Deactivate if requested (User::new always sets active=true)
|
||||
if let Some(false) = dto.active {
|
||||
self.user_storage.set_user_active_status(created.id(), false).await?;
|
||||
}
|
||||
|
||||
// Create personal folder
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
match folder_service.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
}).await {
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for admin-created user {}: {} (ID: {})",
|
||||
created.id(), folder.name, folder.id
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Could not create personal folder for user {}: {}",
|
||||
created.id(), e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Admin created user: {} ({})", dto.username, created.id());
|
||||
Ok(UserDto::from(created))
|
||||
}
|
||||
|
||||
/// Admin-only: reset a user's password.
|
||||
pub async fn admin_reset_password(
|
||||
&self,
|
||||
user_id: &str,
|
||||
new_password: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
if new_password.len() < 8 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput, "User",
|
||||
"Password must be at least 8 characters long".to_string(),
|
||||
));
|
||||
}
|
||||
let hash = self.password_hasher.hash_password(new_password)?;
|
||||
self.user_storage.change_password(user_id, &hash).await
|
||||
}
|
||||
|
||||
/// Get a single user by ID (for admin panel)
|
||||
pub async fn get_user_admin(&self, user_id: &str) -> Result<UserDto, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::application::dtos::settings_dto::{
|
||||
SaveOidcSettingsDto, TestOidcConnectionDto,
|
||||
UpdateUserRoleDto, UpdateUserActiveDto, UpdateUserQuotaDto,
|
||||
ListUsersQueryDto, DashboardStatsDto,
|
||||
AdminCreateUserDto, AdminResetPasswordDto,
|
||||
};
|
||||
use crate::interfaces::errors::AppError;
|
||||
|
||||
@@ -26,11 +27,16 @@ pub fn admin_routes() -> Router<AppState> {
|
||||
.route("/dashboard", get(get_dashboard_stats))
|
||||
// User management
|
||||
.route("/users", get(list_users))
|
||||
.route("/users", post(create_user))
|
||||
.route("/users/{id}", get(get_user))
|
||||
.route("/users/{id}", delete(delete_user))
|
||||
.route("/users/{id}/role", put(update_user_role))
|
||||
.route("/users/{id}/active", put(update_user_active))
|
||||
.route("/users/{id}/quota", put(update_user_quota))
|
||||
.route("/users/{id}/password", put(reset_user_password))
|
||||
// Registration control
|
||||
.route("/settings/registration", get(get_registration_setting))
|
||||
.route("/settings/registration", put(set_registration_setting))
|
||||
}
|
||||
|
||||
/// Validate JWT and require admin role. Returns (user_id, role).
|
||||
@@ -191,6 +197,13 @@ async fn get_dashboard_stats(
|
||||
storage_usage_percent: (usage_percent * 100.0).round() / 100.0,
|
||||
users_over_80_percent: stats_row.get("users_over_80"),
|
||||
users_over_quota: stats_row.get("users_over_quota"),
|
||||
registration_enabled: {
|
||||
if let Some(svc) = state.admin_settings_service.as_ref() {
|
||||
svc.get_registration_enabled().await
|
||||
} else {
|
||||
true // default: enabled
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(stats))
|
||||
@@ -351,3 +364,101 @@ async fn update_user_quota(
|
||||
"quota_bytes": dto.quota_bytes,
|
||||
}))))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Admin User Creation & Password Reset
|
||||
// ============================================================================
|
||||
|
||||
/// POST /api/admin/users — create a new user (admin only)
|
||||
async fn create_user(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(dto): Json<AdminCreateUserDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
let user = auth.auth_application_service.admin_create_user(dto).await
|
||||
.map_err(|e| AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("Failed to create user: {}", e),
|
||||
"CreateUserFailed",
|
||||
))?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(user)))
|
||||
}
|
||||
|
||||
/// PUT /api/admin/users/:id/password — reset a user's password (admin only)
|
||||
async fn reset_user_password(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<AdminResetPasswordDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let auth = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||
|
||||
auth.auth_application_service.admin_reset_password(&id, &dto.new_password).await
|
||||
.map_err(|e| AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("Failed to reset password: {}", e),
|
||||
"ResetPasswordFailed",
|
||||
))?;
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "Password reset successfully"
|
||||
}))))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Control
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/settings/registration — check if public registration is enabled
|
||||
async fn get_registration_setting(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
|
||||
let svc = state.admin_settings_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
||||
|
||||
let val = svc.get_registration_enabled().await;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"registration_enabled": val,
|
||||
})))
|
||||
}
|
||||
|
||||
/// PUT /api/admin/settings/registration — enable/disable public registration
|
||||
async fn set_registration_setting(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
||||
|
||||
let enabled = body.get("registration_enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.ok_or_else(|| AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Missing boolean field 'registration_enabled'",
|
||||
"InvalidInput",
|
||||
))?;
|
||||
|
||||
let svc = state.admin_settings_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
||||
|
||||
svc.set_registration_enabled(enabled, &admin_id).await
|
||||
.map_err(|e| AppError::internal_error(&format!("Failed to save setting: {}", e)))?;
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({
|
||||
"message": format!("Public registration {}", if enabled { "enabled" } else { "disabled" }),
|
||||
"registration_enabled": enabled,
|
||||
}))))
|
||||
}
|
||||
|
||||
@@ -65,6 +65,17 @@ async fn register(
|
||||
"PasswordRegistrationDisabled",
|
||||
));
|
||||
}
|
||||
|
||||
// Check if public registration has been disabled by the admin
|
||||
if let Some(admin_svc) = state.admin_settings_service.as_ref() {
|
||||
if !admin_svc.get_registration_enabled().await {
|
||||
return Err(AppError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Public registration has been disabled by the administrator.",
|
||||
"RegistrationDisabled",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Registration logic (admin detection, fresh-install handling, duplicate
|
||||
// checks) is all inside the service layer. Call it directly.
|
||||
|
||||
+177
-1
@@ -262,13 +262,18 @@ details[open] summary{margin-bottom:14px;color:#ff5e3a}
|
||||
<div class="stat-card"><div class="stat-value" id="ds-oidc">—</div><div class="stat-label">OIDC</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="ds-quotas-flag">—</div><div class="stat-label">Quotas</div></div>
|
||||
</div>
|
||||
<div class="toggle-row" style="margin-top:10px;padding:12px 0;border-top:1px solid #e2e8f0">
|
||||
<label><i class="fas fa-user-plus" style="color:#64748b;margin-right:6px"></i> Allow public self-registration</label>
|
||||
<label class="switch"><input type="checkbox" id="ds-registration" checked onchange="toggleRegistration(this.checked)"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="warning" id="registration-warning" style="display:none"><i class="fas fa-exclamation-triangle"></i> Public registration is disabled. Only admins can create new users.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======== USERS TAB ======== -->
|
||||
<div id="tab-users" class="tab-content">
|
||||
<div class="admin-card">
|
||||
<h2><i class="fas fa-users-cog"></i> User Management</h2>
|
||||
<h2 style="justify-content:space-between"><span><i class="fas fa-users-cog"></i> User Management</span><button class="btn btn-primary" onclick="openCreateUserModal()"><i class="fas fa-user-plus"></i> Create User</button></h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -383,6 +388,66 @@ details[open] summary{margin-bottom:14px;color:#ff5e3a}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create User Modal -->
|
||||
<div id="create-user-modal" class="modal-overlay" style="display:none">
|
||||
<div class="modal">
|
||||
<h3><i class="fas fa-user-plus" style="color:#ff5e3a;margin-right:8px"></i> Create New User</h3>
|
||||
<div class="form-group">
|
||||
<label>Username *</label>
|
||||
<input type="text" id="cu-username" placeholder="johndoe" minlength="3" maxlength="32">
|
||||
<small>3–32 characters</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password *</label>
|
||||
<input type="password" id="cu-password" placeholder="Min 8 characters" minlength="8">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email <small style="font-weight:400;color:#94a3b8">(optional)</small></label>
|
||||
<input type="text" id="cu-email" placeholder="user@example.com (auto-generated if empty)">
|
||||
</div>
|
||||
<div style="display:flex;gap:14px">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Role</label>
|
||||
<select id="cu-role" style="width:100%;padding:10px 14px;border:2px solid #e2e8f0;border-radius:10px;font-size:14px;background:#f8fafc">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Quota</label>
|
||||
<div style="display:flex;gap:6px">
|
||||
<input type="number" id="cu-quota-value" min="0" step="1" value="1" style="flex:1">
|
||||
<select id="cu-quota-unit" style="width:70px;padding:10px 8px;border:2px solid #e2e8f0;border-radius:10px;font-size:13px;background:#f8fafc"><option value="1073741824">GB</option><option value="1048576">MB</option><option value="1099511627776">TB</option></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cu-error" class="alert" style="margin-top:0"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="closeCreateUserModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="cu-submit" onclick="submitCreateUser()"><i class="fas fa-user-plus"></i> Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Password Modal -->
|
||||
<div id="reset-pw-modal" class="modal-overlay" style="display:none">
|
||||
<div class="modal">
|
||||
<h3><i class="fas fa-key" style="color:#ff5e3a;margin-right:8px"></i> Reset Password</h3>
|
||||
<div class="form-group">
|
||||
<label>User: <strong id="rp-username"></strong></label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>New Password</label>
|
||||
<input type="password" id="rp-password" placeholder="Min 8 characters" minlength="8">
|
||||
</div>
|
||||
<div id="rp-error" class="alert" style="margin-top:0"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="closeResetPasswordModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="rp-submit" onclick="submitResetPassword()"><i class="fas fa-save"></i> Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/api';
|
||||
const token = localStorage.getItem('oxicloud_token') || localStorage.getItem('token') || localStorage.getItem('access_token');
|
||||
@@ -444,6 +509,12 @@ async function loadDashboard() {
|
||||
document.getElementById('ds-oidc').textContent = d.oidc_configured ? 'Active' : 'Off';
|
||||
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? 'Enabled' : 'Disabled';
|
||||
|
||||
// Registration toggle
|
||||
if (typeof d.registration_enabled !== 'undefined') {
|
||||
document.getElementById('ds-registration').checked = d.registration_enabled;
|
||||
document.getElementById('registration-warning').style.display = d.registration_enabled ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
if (d.users_over_80_percent > 0) {
|
||||
document.getElementById('ds-warn-card').style.display = '';
|
||||
document.getElementById('ds-over80').textContent = d.users_over_80_percent;
|
||||
@@ -480,6 +551,7 @@ async function loadUsers() {
|
||||
'<td style="font-size:12px;color:#94a3b8">' + timeAgo(u.last_login_at) + '</td>' +
|
||||
'<td><div class="actions-row">' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="openQuotaModal(\'' + u.id + '\',\'' + u.username + '\',' + u.storage_quota_bytes + ')" title="Edit quota"><i class="fas fa-box"></i></button>' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="openResetPasswordModal(\'' + u.id + '\',\'' + u.username + '\')" title="Reset password"><i class="fas fa-key"></i></button>' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="toggleRole(\'' + u.id + '\',\'' + u.role + '\')" title="Toggle role"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-' + (u.role === 'admin' ? 'user' : 'crown') + '"></i></button>' +
|
||||
'<button class="btn btn-sm ' + (u.active ? 'btn-danger' : 'btn-success') + '" onclick="toggleActive(\'' + u.id + '\',' + u.active + ')" title="' + (u.active ? 'Deactivate' : 'Activate') + '"' + (isSelf && u.active ? ' disabled' : '') + '><i class="fas fa-' + (u.active ? 'ban' : 'check') + '"></i></button>' +
|
||||
'<button class="btn btn-sm btn-danger" onclick="deleteUser(\'' + u.id + '\',\'' + u.username + '\')" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
|
||||
@@ -552,6 +624,110 @@ async function saveQuota() {
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
}
|
||||
|
||||
// ── Create User Modal ──
|
||||
function openCreateUserModal() {
|
||||
document.getElementById('cu-username').value = '';
|
||||
document.getElementById('cu-password').value = '';
|
||||
document.getElementById('cu-email').value = '';
|
||||
document.getElementById('cu-role').value = 'user';
|
||||
document.getElementById('cu-quota-value').value = '1';
|
||||
document.getElementById('cu-quota-unit').value = '1073741824';
|
||||
document.getElementById('cu-error').className = 'alert';
|
||||
document.getElementById('cu-error').textContent = '';
|
||||
document.getElementById('create-user-modal').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('cu-username').focus(), 100);
|
||||
}
|
||||
function closeCreateUserModal() { document.getElementById('create-user-modal').style.display = 'none'; }
|
||||
|
||||
async function submitCreateUser() {
|
||||
const username = document.getElementById('cu-username').value.trim();
|
||||
const password = document.getElementById('cu-password').value;
|
||||
const email = document.getElementById('cu-email').value.trim() || null;
|
||||
const role = document.getElementById('cu-role').value;
|
||||
const quotaVal = parseFloat(document.getElementById('cu-quota-value').value) || 0;
|
||||
const quotaUnit = parseInt(document.getElementById('cu-quota-unit').value);
|
||||
const quotaBytes = Math.round(quotaVal * quotaUnit);
|
||||
|
||||
const errorEl = document.getElementById('cu-error');
|
||||
if (username.length < 3) { errorEl.textContent = 'Username must be at least 3 characters'; errorEl.className = 'alert alert-error'; return; }
|
||||
if (password.length < 8) { errorEl.textContent = 'Password must be at least 8 characters'; errorEl.className = 'alert alert-error'; return; }
|
||||
|
||||
const btn = document.getElementById('cu-submit');
|
||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating…';
|
||||
try {
|
||||
const resp = await fetch(API + '/admin/users', {
|
||||
method: 'POST', headers: headers(),
|
||||
body: JSON.stringify({ username, password, email, role, quota_bytes: quotaBytes })
|
||||
});
|
||||
if (resp.ok) {
|
||||
closeCreateUserModal();
|
||||
loadUsers();
|
||||
loadDashboard();
|
||||
} else {
|
||||
const e = await resp.json().catch(() => ({}));
|
||||
errorEl.textContent = e.message || 'Failed to create user';
|
||||
errorEl.className = 'alert alert-error';
|
||||
}
|
||||
} catch (e) {
|
||||
errorEl.textContent = 'Network error: ' + e.message;
|
||||
errorEl.className = 'alert alert-error';
|
||||
}
|
||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus"></i> Create';
|
||||
}
|
||||
|
||||
// ── Reset Password Modal ──
|
||||
let resetPwUserId = '';
|
||||
function openResetPasswordModal(userId, username) {
|
||||
resetPwUserId = userId;
|
||||
document.getElementById('rp-username').textContent = username;
|
||||
document.getElementById('rp-password').value = '';
|
||||
document.getElementById('rp-error').className = 'alert';
|
||||
document.getElementById('rp-error').textContent = '';
|
||||
document.getElementById('reset-pw-modal').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('rp-password').focus(), 100);
|
||||
}
|
||||
function closeResetPasswordModal() { document.getElementById('reset-pw-modal').style.display = 'none'; }
|
||||
|
||||
async function submitResetPassword() {
|
||||
const password = document.getElementById('rp-password').value;
|
||||
const errorEl = document.getElementById('rp-error');
|
||||
if (password.length < 8) { errorEl.textContent = 'Password must be at least 8 characters'; errorEl.className = 'alert alert-error'; return; }
|
||||
|
||||
const btn = document.getElementById('rp-submit');
|
||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Resetting…';
|
||||
try {
|
||||
const resp = await fetch(API + '/admin/users/' + resetPwUserId + '/password', {
|
||||
method: 'PUT', headers: headers(),
|
||||
body: JSON.stringify({ new_password: password })
|
||||
});
|
||||
if (resp.ok) { closeResetPasswordModal(); }
|
||||
else { const e = await resp.json().catch(() => ({})); errorEl.textContent = e.message || 'Failed'; errorEl.className = 'alert alert-error'; }
|
||||
} catch (e) { errorEl.textContent = 'Error: ' + e.message; errorEl.className = 'alert alert-error'; }
|
||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> Reset';
|
||||
}
|
||||
|
||||
// ── Registration Toggle ──
|
||||
async function toggleRegistration(enabled) {
|
||||
document.getElementById('registration-warning').style.display = enabled ? 'none' : 'flex';
|
||||
try {
|
||||
const resp = await fetch(API + '/admin/settings/registration', {
|
||||
method: 'PUT', headers: headers(),
|
||||
body: JSON.stringify({ registration_enabled: enabled })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
// Revert toggle on failure
|
||||
document.getElementById('ds-registration').checked = !enabled;
|
||||
document.getElementById('registration-warning').style.display = !enabled ? 'flex' : 'none';
|
||||
const e = await resp.json().catch(() => ({}));
|
||||
alert(e.message || 'Failed to update registration setting');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('ds-registration').checked = !enabled;
|
||||
document.getElementById('registration-warning').style.display = !enabled ? 'flex' : 'none';
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── OIDC settings ──
|
||||
document.getElementById('oidc-enabled').addEventListener('change', function() {
|
||||
document.getElementById('oidc-form').style.display = this.checked ? 'block' : 'none';
|
||||
|
||||
Reference in New Issue
Block a user