Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66c08e8bc6 | |||
| 17acb7d018 | |||
| e73b3147ed |
@ -50,6 +50,13 @@ type LoginRequest struct {
|
|||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupRequest 初始管理员注册请求
|
||||||
|
type SetupRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshTokenRequest 刷新 Token 请求
|
// RefreshTokenRequest 刷新 Token 请求
|
||||||
type RefreshTokenRequest struct {
|
type RefreshTokenRequest struct {
|
||||||
RefreshToken string `json:"refreshToken" binding:"required"`
|
RefreshToken string `json:"refreshToken" binding:"required"`
|
||||||
|
|||||||
@ -265,11 +265,7 @@ func (h *AuthHandler) AuthStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Setup creates the first admin user. Only works when no admin exists.
|
// Setup creates the first admin user. Only works when no admin exists.
|
||||||
func (h *AuthHandler) Setup(w http.ResponseWriter, r *http.Request) {
|
func (h *AuthHandler) Setup(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req dto.SetupRequest
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||||
return
|
return
|
||||||
@ -279,19 +275,12 @@ func (h *AuthHandler) Setup(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := h.authService.SetupInitialAdmin(r.Context(), req.Username, req.Password, req.Email)
|
_, accessToken, refreshToken, err := h.authService.SetupInitialAdmin(r.Context(), req.Username, req.Password, req.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondServiceError(w, err, "Failed to create initial admin")
|
respondServiceError(w, err, "Failed to create initial admin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate access token immediately
|
|
||||||
accessToken, refreshToken, _, err := h.authService.Login(r.Context(), req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
respondServiceError(w, err, "Admin created but login failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJSON(w, http.StatusCreated, map[string]string{
|
respondJSON(w, http.StatusCreated, map[string]string{
|
||||||
"accessToken": accessToken,
|
"accessToken": accessToken,
|
||||||
"refreshToken": refreshToken,
|
"refreshToken": refreshToken,
|
||||||
|
|||||||
@ -85,6 +85,17 @@ func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryMock) AdminExists(ctx context.Context) (bool, error) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
for _, u := range r.users {
|
||||||
|
if u.Role == "admin" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
|
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|||||||
@ -120,6 +120,12 @@ func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update 更新用户
|
// Update 更新用户
|
||||||
|
func (r *UserRepository) AdminExists(ctx context.Context) (bool, error) {
|
||||||
|
var exists bool
|
||||||
|
err := r.db.conn.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE role = 'admin')`).Scan(&exists)
|
||||||
|
return exists, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
|
func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
|
||||||
user.UpdatedAt = time.Now()
|
user.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
|||||||
@ -24,4 +24,7 @@ type UserRepository interface {
|
|||||||
|
|
||||||
// List 列出所有用户
|
// List 列出所有用户
|
||||||
List(ctx context.Context) ([]*entity.User, error)
|
List(ctx context.Context) ([]*entity.User, error)
|
||||||
|
|
||||||
|
// AdminExists checks whether any admin user exists (lightweight EXISTS query)
|
||||||
|
AdminExists(ctx context.Context) (bool, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,53 +80,53 @@ type UserWorkspaceOptions struct {
|
|||||||
QuotaGPUMem string
|
QuotaGPUMem string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAdminExists checks whether any admin user already exists in the database.
|
func defaultEmail(username string) string {
|
||||||
func (s *AuthService) IsAdminExists(ctx context.Context) (bool, error) {
|
return username + "@local.ocdp"
|
||||||
users, err := s.userRepo.List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for _, u := range users {
|
|
||||||
if u.Role == authz.RoleAdmin {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupInitialAdmin creates the first admin user. Fails if an admin already exists.
|
// IsAdminExists checks whether any admin user already exists in the database.
|
||||||
func (s *AuthService) SetupInitialAdmin(ctx context.Context, username, password, email string) (*entity.User, error) {
|
func (s *AuthService) IsAdminExists(ctx context.Context) (bool, error) {
|
||||||
|
return s.userRepo.AdminExists(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupInitialAdmin creates the first admin user and returns access + refresh tokens.
|
||||||
|
// Fails if an admin already exists.
|
||||||
|
func (s *AuthService) SetupInitialAdmin(ctx context.Context, username, password, email string) (*entity.User, string, string, error) {
|
||||||
hasAdmin, err := s.IsAdminExists(ctx)
|
hasAdmin, err := s.IsAdminExists(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", "", err
|
||||||
}
|
}
|
||||||
if hasAdmin {
|
if hasAdmin {
|
||||||
return nil, entity.ErrForbidden
|
return nil, "", "", entity.ErrForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
|
||||||
passwordHash, err := s.passwordHasher.Hash(password)
|
passwordHash, err := s.passwordHasher.Hash(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = username + "@local.ocdp"
|
email = defaultEmail(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := entity.NewUser(username, passwordHash, email)
|
user := entity.NewUser(username, passwordHash, email)
|
||||||
user.ID = uuid.New().String()
|
user.ID = uuid.New().String()
|
||||||
user.Role = authz.RoleAdmin
|
user.Role = authz.RoleAdmin
|
||||||
user.WorkspaceID = entity.DefaultWorkspaceID
|
user.WorkspaceID = entity.DefaultWorkspaceID
|
||||||
user.IsActive = true
|
|
||||||
|
|
||||||
if err := user.Validate(); err != nil {
|
if err := user.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, "", "", err
|
||||||
}
|
}
|
||||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||||
return nil, err
|
return nil, "", "", err
|
||||||
}
|
}
|
||||||
return user, nil
|
|
||||||
|
// Generate tokens directly — avoid a separate login round-trip
|
||||||
|
accessToken, refreshToken, err := s.tokenGenerator.Generate(user.ID, user.Username, user.Role, user.WorkspaceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", err
|
||||||
|
}
|
||||||
|
return user, accessToken, refreshToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) Register(ctx context.Context, username, password, role, workspaceID string, opts UserWorkspaceOptions, isActive, mustChangePassword *bool) (*entity.User, error) {
|
func (s *AuthService) Register(ctx context.Context, username, password, role, workspaceID string, opts UserWorkspaceOptions, isActive, mustChangePassword *bool) (*entity.User, error) {
|
||||||
@ -158,7 +158,7 @@ func (s *AuthService) Register(ctx context.Context, username, password, role, wo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 默认生成占位邮箱,避免数据库约束失败
|
// 默认生成占位邮箱,避免数据库约束失败
|
||||||
email := username + "@local.ocdp"
|
email := defaultEmail(username)
|
||||||
|
|
||||||
// 创建用户
|
// 创建用户
|
||||||
user := entity.NewUser(username, passwordHash, email)
|
user := entity.NewUser(username, passwordHash, email)
|
||||||
|
|||||||
@ -98,6 +98,7 @@ services:
|
|||||||
sh -c "
|
sh -c "
|
||||||
set -eux;
|
set -eux;
|
||||||
npm ci;
|
npm ci;
|
||||||
|
rm -rf node_modules/.tmp;
|
||||||
npm run build;
|
npm run build;
|
||||||
mkdir -p /build;
|
mkdir -p /build;
|
||||||
rm -rf /build/*;
|
rm -rf /build/*;
|
||||||
|
|||||||
@ -220,9 +220,9 @@ export const login = postAuthLogin;
|
|||||||
export const register = postAuthRegister;
|
export const register = postAuthRegister;
|
||||||
export const refreshAuth = postAuthRefresh;
|
export const refreshAuth = postAuthRefresh;
|
||||||
export const fetchAuthStatus = () =>
|
export const fetchAuthStatus = () =>
|
||||||
AXIOS_INSTANCE.get<{ needsSetup: boolean; hasUsers: boolean }>("/auth/status").then((r) => r.data);
|
customAxiosInstance<{ needsSetup: boolean; hasUsers: boolean }>({ url: "/auth/status", method: "GET" });
|
||||||
export const setupInitialAdmin = (data: { username: string; password: string; email?: string }) =>
|
export const setupInitialAdmin = (data: { username: string; password: string; email?: string }) =>
|
||||||
AXIOS_INSTANCE.post<{ accessToken: string; refreshToken: string }>("/auth/setup", data).then((r) => r.data);
|
customAxiosInstance<{ accessToken: string; refreshToken: string }>({ url: "/auth/setup", method: "POST", data });
|
||||||
export const listUsers = () => customAxiosInstance<UserResponse[]>({ url: "/users", method: "GET" });
|
export const listUsers = () => customAxiosInstance<UserResponse[]>({ url: "/users", method: "GET" });
|
||||||
export const createUser = (data: AdminCreateUserRequest) =>
|
export const createUser = (data: AdminCreateUserRequest) =>
|
||||||
customAxiosInstance<UserResponse>({ url: "/users", method: "POST", data });
|
customAxiosInstance<UserResponse>({ url: "/users", method: "POST", data });
|
||||||
|
|||||||
@ -59,20 +59,20 @@ const AuthPage: React.FC<Props> = ({ onLogin }) => {
|
|||||||
toastInfo("Creating admin account...", { title: "Setup", durationMs: 1200 });
|
toastInfo("Creating admin account...", { title: "Setup", durationMs: 1200 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setupInitialAdmin({
|
const result = await setupInitialAdmin({
|
||||||
username: setupUsername,
|
username: setupUsername,
|
||||||
password: setupPassword,
|
password: setupPassword,
|
||||||
email: setupEmail || undefined,
|
email: setupEmail || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Login with the returned tokens
|
// setupInitialAdmin returns tokens — use them directly to avoid redundant login
|
||||||
const loginResponse = await apiLogin({
|
onLogin({
|
||||||
|
accessToken: result.accessToken,
|
||||||
|
refreshToken: result.refreshToken,
|
||||||
username: setupUsername,
|
username: setupUsername,
|
||||||
password: setupPassword,
|
} as any);
|
||||||
});
|
|
||||||
|
|
||||||
toastSuccess("Admin account created. Welcome!");
|
toastSuccess("Admin account created. Welcome!");
|
||||||
onLogin(loginResponse);
|
|
||||||
navigate("/home", { replace: true });
|
navigate("/home", { replace: true });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = getErrorMessage(err, "Setup failed. Please try again later.");
|
const msg = getErrorMessage(err, "Setup failed. Please try again later.");
|
||||||
|
|||||||
Reference in New Issue
Block a user