Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion backend/app/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import secrets
from datetime import datetime, timedelta

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.core.dependencies import get_current_user, get_current_active_superuser
from app.core.security import create_access_token, verify_password, get_password_hash
from app.config import settings
from app.database import get_db
from app.models import User
from app.models.password_reset import PasswordResetToken
from app.schemas import (
LoginRequest, Token, UserCreate, UserOut, UserInfoResponse,
LoginRequest, Token, UserCreate, UserUpdate, UserOut, UserInfoResponse,
InitialSetupRequest, PasswordResetRequest,
PasswordResetConfirm, SystemStatusResponse,
)

Check failure on line 17 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

app/api/auth.py:1:1: I001 Import block is un-sorted or un-formatted

router = APIRouter()

Expand Down Expand Up @@ -200,9 +200,9 @@
"""
Get current user information and their accessible communities with roles.
"""
from app.core.dependencies import get_user_community_role
from app.schemas.community import CommunityWithRole
from app.models.community import Community

Check failure on line 205 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

app/api/auth.py:203:5: I001 Import block is un-sorted or un-formatted

# Superusers can see all communities
if current_user.is_superuser:
Expand All @@ -225,7 +225,7 @@
role=role or "user",
)
)

Check failure on line 228 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

app/api/auth.py:228:1: W293 Blank line contains whitespace
return UserInfoResponse(
user=current_user,
communities=communities_with_roles
Expand All @@ -245,6 +245,102 @@
return users


@router.patch("/users/{user_id}", response_model=UserOut)
def update_user(
user_id: int,
user_update: UserUpdate,
current_user: User = Depends(get_current_active_superuser),
db: Session = Depends(get_db),
):
"""
Update a user's information. Only superusers can update users.
"""
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)

# Prevent demoting yourself
if user_update.is_superuser is False and target_user.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能取消自己的超级管理员权限",
)

# Prevent demoting the last superuser
if user_update.is_superuser is False and target_user.is_superuser:
superuser_count = db.query(User).filter(
User.is_superuser == True, User.is_default_admin == False # noqa: E712
).count()
if superuser_count <= 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能取消最后一个超级管理员的权限",
)

# Check email uniqueness if updating email
if user_update.email is not None:
existing = db.query(User).filter(
User.email == user_update.email, User.id != user_id
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该邮箱已被其他用户使用",
)

# Apply updates
update_data = user_update.model_dump(exclude_unset=True)
if "password" in update_data:
target_user.hashed_password = get_password_hash(update_data.pop("password"))
for field, value in update_data.items():
setattr(target_user, field, value)

db.commit()
db.refresh(target_user)
return target_user


@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(
user_id: int,
current_user: User = Depends(get_current_active_superuser),
db: Session = Depends(get_db),
):
"""
Delete a user. Only superusers can delete users.
"""
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)

# Cannot delete yourself
if target_user.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能删除自己的账号",
)

# Cannot delete the last superuser
if target_user.is_superuser:
superuser_count = db.query(User).filter(
User.is_superuser == True, User.is_default_admin == False # noqa: E712
).count()
if superuser_count <= 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能删除最后一个超级管理员",
)

db.delete(target_user)
db.commit()


@router.post("/password-reset/request", status_code=status.HTTP_200_OK)
def request_password_reset(
reset_request: PasswordResetRequest,
Expand Down Expand Up @@ -341,9 +437,9 @@

def _send_password_reset_email(to_email: str, token: str) -> None:
"""Send a password reset email via SMTP."""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

Check failure on line 442 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

app/api/auth.py:440:5: I001 Import block is un-sorted or un-formatted

reset_url = f"{settings.FRONTEND_URL}/reset-password?token={token}"

Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class UserUpdate(BaseModel):
full_name: Optional[str] = None
password: Optional[str] = Field(None, min_length=6, max_length=100)
is_active: Optional[bool] = None
is_superuser: Optional[bool] = None


class UserOut(UserBase):
Expand Down
45 changes: 0 additions & 45 deletions frontend/public/logo.svg

This file was deleted.

14 changes: 14 additions & 0 deletions frontend/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,17 @@ export async function listAllUsers(): Promise<User[]> {
const { data } = await apiClient.get<User[]>('/auth/users')
return data
}

export async function updateUser(userId: number, userData: {
email?: string
full_name?: string
is_superuser?: boolean
is_active?: boolean
}): Promise<User> {
const { data } = await apiClient.patch<User>(`/auth/users/${userId}`, userData)
return data
}

export async function deleteUser(userId: number): Promise<void> {
await apiClient.delete(`/auth/users/${userId}`)
}
3 changes: 3 additions & 0 deletions frontend/src/views/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ const handleLogin = async () => {
height: auto;
margin: 0 auto 16px;
display: block;
background-color: #fff;
border-radius: 8px;
padding: 8px;
}

h2 {
Expand Down
128 changes: 126 additions & 2 deletions frontend/src/views/UserManage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="showEditDialog(row)">编辑</el-button>
<el-button
v-if="row.id !== currentUser?.id"
size="small"
type="danger"
@click="handleDelete(row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>

Expand Down Expand Up @@ -71,21 +82,64 @@
<el-button type="primary" @click="handleRegister" :loading="submitting">注册</el-button>
</template>
</el-dialog>

<!-- Edit User Dialog -->
<el-dialog v-model="editDialogVisible" title="编辑用户" width="480px">
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="80px"
>
<el-form-item label="用户名">
<el-input :model-value="editForm.username" disabled />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="editForm.email" placeholder="邮箱地址" />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="editForm.full_name" placeholder="姓名(可选)" />
</el-form-item>
<el-form-item label="状态">
<el-switch
v-model="editForm.is_active"
active-text="活跃"
inactive-text="禁用"
:disabled="editForm.id === currentUser?.id"
/>
</el-form-item>
<el-form-item label="用户类型">
<el-checkbox v-model="editForm.is_superuser">
超级管理员
</el-checkbox>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEdit" :loading="submitting">保存</el-button>
</template>
</el-dialog>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { listAllUsers, register } from '../api/auth'
import { listAllUsers, register, updateUser, deleteUser } from '../api/auth'
import { useAuthStore } from '../stores/auth'
import type { User } from '../stores/auth'

const authStore = useAuthStore()
const currentUser = authStore.user

const users = ref<User[]>([])
const loading = ref(false)
const registerDialogVisible = ref(false)
const editDialogVisible = ref(false)
const submitting = ref(false)
const registerFormRef = ref<FormInstance>()
const editFormRef = ref<FormInstance>()

const registerForm = ref({
username: '',
Expand All @@ -95,6 +149,15 @@ const registerForm = ref({
is_superuser: false,
})

const editForm = ref({
id: 0,
username: '',
email: '',
full_name: '',
is_active: true,
is_superuser: false,
})

const registerRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
Expand All @@ -110,6 +173,13 @@ const registerRules: FormRules = {
],
}

const editRules: FormRules = {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' },
],
}

async function loadUsers() {
loading.value = true
try {
Expand All @@ -126,6 +196,18 @@ function showRegisterDialog() {
registerDialogVisible.value = true
}

function showEditDialog(user: User) {
editForm.value = {
id: user.id,
username: user.username,
email: user.email,
full_name: user.full_name || '',
is_active: user.is_active,
is_superuser: user.is_superuser,
}
editDialogVisible.value = true
}

async function handleRegister() {
if (!registerFormRef.value) return
await registerFormRef.value.validate()
Expand All @@ -149,6 +231,48 @@ async function handleRegister() {
}
}

async function handleEdit() {
if (!editFormRef.value) return
await editFormRef.value.validate()

submitting.value = true
try {
await updateUser(editForm.value.id, {
email: editForm.value.email,
full_name: editForm.value.full_name || undefined,
is_active: editForm.value.is_active,
is_superuser: editForm.value.is_superuser,
})
ElMessage.success('用户信息已更新')
editDialogVisible.value = false
await loadUsers()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '更新失败')
} finally {
submitting.value = false
}
}

async function handleDelete(user: User) {
try {
await ElMessageBox.confirm(
`确定要删除用户「${user.username}」吗?此操作不可恢复。`,
'确认删除',
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' },
)
} catch {
return
}

try {
await deleteUser(user.id)
ElMessage.success('用户已删除')
await loadUsers()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '删除失败')
}
}

function formatDate(d: string) {
return new Date(d).toLocaleString('zh-CN')
}
Expand Down
Loading
Loading