mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-04-29 19:08:58 -05:00
fix admin portal cannot see org issue
This commit is contained in:
6
src/kohaku-hub-admin/src/components.d.ts
vendored
6
src/kohaku-hub-admin/src/components.d.ts
vendored
@@ -24,9 +24,15 @@ declare module 'vue' {
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
|
||||
@@ -20,7 +20,54 @@ const users = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const userDialogVisible = ref(false);
|
||||
const quotaDialogVisible = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
const quotaForm = ref({
|
||||
username: "",
|
||||
private_quota_bytes: null,
|
||||
public_quota_bytes: null,
|
||||
});
|
||||
|
||||
const quotaInputPrivate = ref("");
|
||||
const quotaInputPublic = ref("");
|
||||
|
||||
// Parse human-readable size (100G, 5TB, 500MB) to bytes (decimal 1000^k)
|
||||
function parseHumanSize(input) {
|
||||
if (!input || input === "null" || input === "unlimited") return null;
|
||||
|
||||
const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B?)?$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = (match[2] || "").toUpperCase().replace("B", "");
|
||||
|
||||
const multipliers = {
|
||||
"": 1,
|
||||
K: 1000,
|
||||
M: 1000000,
|
||||
G: 1000000000,
|
||||
T: 1000000000000,
|
||||
};
|
||||
|
||||
return Math.floor(value * (multipliers[unit] || 1));
|
||||
}
|
||||
|
||||
// Format bytes to human-readable (decimal)
|
||||
function formatBytesDecimal(bytes) {
|
||||
if (bytes === null || bytes === undefined) return "Unlimited";
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1000 && unitIndex < units.length - 1) {
|
||||
size /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
// Search and filters
|
||||
const searchQuery = ref("");
|
||||
@@ -237,6 +284,70 @@ This action CANNOT be undone!`,
|
||||
}
|
||||
}
|
||||
|
||||
function openQuotaDialog() {
|
||||
quotaForm.value = {
|
||||
username: selectedUser.value.username,
|
||||
private_quota_bytes: selectedUser.value.private_quota_bytes,
|
||||
public_quota_bytes: selectedUser.value.public_quota_bytes,
|
||||
};
|
||||
|
||||
// Set human-readable inputs
|
||||
quotaInputPrivate.value =
|
||||
selectedUser.value.private_quota_bytes !== null
|
||||
? formatBytesDecimal(selectedUser.value.private_quota_bytes)
|
||||
: "unlimited";
|
||||
quotaInputPublic.value =
|
||||
selectedUser.value.public_quota_bytes !== null
|
||||
? formatBytesDecimal(selectedUser.value.public_quota_bytes)
|
||||
: "unlimited";
|
||||
|
||||
quotaDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function updateQuotaFromInput(type) {
|
||||
const input =
|
||||
type === "private" ? quotaInputPrivate.value : quotaInputPublic.value;
|
||||
const bytes = parseHumanSize(input);
|
||||
|
||||
if (type === "private") {
|
||||
quotaForm.value.private_quota_bytes = bytes;
|
||||
} else {
|
||||
quotaForm.value.public_quota_bytes = bytes;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveQuota() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
// Update quota via admin API
|
||||
await fetch(
|
||||
`http://localhost:48888/admin/api/users/${quotaForm.value.username}/quota`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Admin-Token": adminStore.token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
private_quota_bytes: quotaForm.value.private_quota_bytes,
|
||||
public_quota_bytes: quotaForm.value.public_quota_bytes,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
ElMessage.success("Quota updated successfully");
|
||||
quotaDialogVisible.value = false;
|
||||
userDialogVisible.value = false;
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
ElMessage.error("Failed to update quota");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEmailVerification(row) {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
@@ -598,11 +709,159 @@ onMounted(() => {
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="userDialogVisible = false">Close</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="$router.push(`/quotas?namespace=${selectedUser.username}`)"
|
||||
>
|
||||
Manage Quota
|
||||
<el-button type="primary" @click="openQuotaDialog">
|
||||
Edit Quota
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Quota Edit Dialog -->
|
||||
<el-dialog
|
||||
v-model="quotaDialogVisible"
|
||||
title="Edit Storage Quota"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="quotaForm" label-width="150px">
|
||||
<el-form-item label="Username">
|
||||
<span>{{ quotaForm.username }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">Public Quota</el-divider>
|
||||
|
||||
<el-form-item label="Public Quota">
|
||||
<el-input
|
||||
v-model="quotaInputPublic"
|
||||
@blur="updateQuotaFromInput('public')"
|
||||
placeholder="e.g., 10G, 500MB, unlimited"
|
||||
>
|
||||
<template #append>
|
||||
<span class="text-xs"
|
||||
>=
|
||||
{{ formatBytesDecimal(quotaForm.public_quota_bytes) }}</span
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Enter: 100G, 5TB, 500MB, or "unlimited" (decimal: 1GB =
|
||||
1,000,000,000 bytes)
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Quick Presets">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPublic = '5G';
|
||||
updateQuotaFromInput('public');
|
||||
"
|
||||
>5GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPublic = '10G';
|
||||
updateQuotaFromInput('public');
|
||||
"
|
||||
>10GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPublic = '50G';
|
||||
updateQuotaFromInput('public');
|
||||
"
|
||||
>50GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPublic = '100G';
|
||||
updateQuotaFromInput('public');
|
||||
"
|
||||
>100GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPublic = 'unlimited';
|
||||
updateQuotaFromInput('public');
|
||||
"
|
||||
>Unlimited</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">Private Quota</el-divider>
|
||||
|
||||
<el-form-item label="Private Quota">
|
||||
<el-input
|
||||
v-model="quotaInputPrivate"
|
||||
@blur="updateQuotaFromInput('private')"
|
||||
placeholder="e.g., 10G, 500MB, unlimited"
|
||||
>
|
||||
<template #append>
|
||||
<span class="text-xs"
|
||||
>=
|
||||
{{ formatBytesDecimal(quotaForm.private_quota_bytes) }}</span
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Enter: 100G, 5TB, 500MB, or "unlimited"
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Quick Presets">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPrivate = '5G';
|
||||
updateQuotaFromInput('private');
|
||||
"
|
||||
>5GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPrivate = '10G';
|
||||
updateQuotaFromInput('private');
|
||||
"
|
||||
>10GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPrivate = '50G';
|
||||
updateQuotaFromInput('private');
|
||||
"
|
||||
>50GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPrivate = '100G';
|
||||
updateQuotaFromInput('private');
|
||||
"
|
||||
>100GB</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
quotaInputPrivate = 'unlimited';
|
||||
updateQuotaFromInput('private');
|
||||
"
|
||||
>Unlimited</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="quotaDialogVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="saveQuota" :loading="loading">
|
||||
Save
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -21,9 +21,10 @@ class UserInfo(BaseModel):
|
||||
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
email: str | None # Nullable for organizations
|
||||
email_verified: bool
|
||||
is_active: bool
|
||||
is_org: bool # Add org flag
|
||||
private_quota_bytes: int | None
|
||||
public_quota_bytes: int | None
|
||||
private_used_bytes: int
|
||||
@@ -51,23 +52,23 @@ async def get_user_info(
|
||||
username: str,
|
||||
_admin: bool = Depends(verify_admin_token),
|
||||
):
|
||||
"""Get detailed user information.
|
||||
"""Get detailed user or organization information.
|
||||
|
||||
Args:
|
||||
username: Username to query
|
||||
username: Username or org name to query
|
||||
_admin: Admin authentication (dependency)
|
||||
|
||||
Returns:
|
||||
User information
|
||||
User/org information
|
||||
|
||||
Raises:
|
||||
HTTPException: If user not found
|
||||
HTTPException: If user/org not found
|
||||
"""
|
||||
|
||||
user = User.get_or_none(User.username == username)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(404, detail={"error": f"User not found: {username}"})
|
||||
raise HTTPException(404, detail={"error": f"User/org not found: {username}"})
|
||||
|
||||
return UserInfo(
|
||||
id=user.id,
|
||||
@@ -75,6 +76,7 @@ async def get_user_info(
|
||||
email=user.email,
|
||||
email_verified=user.email_verified,
|
||||
is_active=user.is_active,
|
||||
is_org=user.is_org, # Add org flag
|
||||
private_quota_bytes=user.private_quota_bytes,
|
||||
public_quota_bytes=user.public_quota_bytes,
|
||||
private_used_bytes=user.private_used_bytes,
|
||||
@@ -301,3 +303,52 @@ async def set_email_verification(
|
||||
"email": user.email,
|
||||
"email_verified": user.email_verified,
|
||||
}
|
||||
|
||||
|
||||
class UpdateQuotaRequest(BaseModel):
|
||||
"""Request to update user/org quota."""
|
||||
|
||||
private_quota_bytes: int | None = None
|
||||
public_quota_bytes: int | None = None
|
||||
|
||||
|
||||
@router.put("/users/{username}/quota")
|
||||
async def update_user_quota(
|
||||
username: str,
|
||||
request: UpdateQuotaRequest,
|
||||
_admin: bool = Depends(verify_admin_token),
|
||||
):
|
||||
"""Update storage quota for user or organization.
|
||||
|
||||
Args:
|
||||
username: Username or org name
|
||||
request: Quota update request
|
||||
_admin: Admin authentication
|
||||
|
||||
Returns:
|
||||
Updated quota information
|
||||
|
||||
Raises:
|
||||
HTTPException: If user/org not found
|
||||
"""
|
||||
user = User.get_or_none(User.username == username)
|
||||
if not user:
|
||||
raise HTTPException(404, detail={"error": f"User/org not found: {username}"})
|
||||
|
||||
# Update quotas
|
||||
user.private_quota_bytes = request.private_quota_bytes
|
||||
user.public_quota_bytes = request.public_quota_bytes
|
||||
user.save()
|
||||
|
||||
logger.info(
|
||||
f"Admin updated quota for {username}: "
|
||||
f"private={request.private_quota_bytes}, public={request.public_quota_bytes}"
|
||||
)
|
||||
|
||||
return {
|
||||
"username": user.username,
|
||||
"private_quota_bytes": user.private_quota_bytes,
|
||||
"public_quota_bytes": user.public_quota_bytes,
|
||||
"private_used_bytes": user.private_used_bytes,
|
||||
"public_used_bytes": user.public_used_bytes,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user