feat: return not found. on /setup if already completed

This commit is contained in:
Elias Schneider
2026-04-19 20:13:19 +02:00
parent a0cb574313
commit 444f7ff2b0
6 changed files with 64 additions and 16 deletions

View File

@@ -28,10 +28,10 @@ func (e *AlreadyInUseError) Is(target error) bool {
return errors.As(target, &x)
}
type SetupAlreadyCompletedError struct{}
type SetupNotAvailableError struct{}
func (e *SetupAlreadyCompletedError) Error() string { return "setup already completed" }
func (e *SetupAlreadyCompletedError) HttpStatusCode() int { return http.StatusConflict }
func (e *SetupNotAvailableError) Error() string { return "not found" }
func (e *SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound }
type TokenInvalidOrExpiredError struct{}

View File

@@ -7,6 +7,7 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/service"
@@ -30,6 +31,7 @@ func NewUserSignupController(group *gin.RouterGroup, authMiddleware *middleware.
group.GET("/signup-tokens", authMiddleware.Add(), usc.listSignupTokensHandler)
group.DELETE("/signup-tokens/:id", authMiddleware.Add(), usc.deleteSignupTokenHandler)
group.POST("/signup", rateLimitMiddleware.Add(rate.Every(1*time.Minute), 10), usc.signupHandler)
group.GET("/signup/setup", usc.checkInitialAdminSetupAvailable)
group.POST("/signup/setup", usc.signUpInitialAdmin)
}
@@ -39,6 +41,21 @@ type UserSignupController struct {
appConfigService *service.AppConfigService
}
func (usc *UserSignupController) checkInitialAdminSetupAvailable(c *gin.Context) {
setupCompleted, err := usc.userSignUpService.IsInitialAdminSetupCompleted(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
}
if setupCompleted {
_ = c.Error(&common.SetupNotAvailableError{})
return
}
c.Status(http.StatusNoContent)
}
// signUpInitialAdmin godoc
// @Summary Sign up initial admin user
// @Description Sign up and generate setup access token for initial admin user

View File

@@ -124,14 +124,12 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
tx.Rollback()
}()
var userCount int64
if err := tx.WithContext(ctx).Model(&model.User{}).
Where("id != ?", staticApiKeyUserID).
Count(&userCount).Error; err != nil {
setupCompleted, err := s.isInitialAdminSetupCompleted(ctx, tx)
if err != nil {
return model.User{}, "", err
}
if userCount != 0 {
return model.User{}, "", &common.SetupAlreadyCompletedError{}
if setupCompleted {
return model.User{}, "", &common.SetupNotAvailableError{}
}
userToCreate := dto.UserCreateDto{
@@ -161,6 +159,21 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d
return user, token, nil
}
func (s *UserSignUpService) IsInitialAdminSetupCompleted(ctx context.Context) (bool, error) {
return s.isInitialAdminSetupCompleted(ctx, s.db)
}
func (s *UserSignUpService) isInitialAdminSetupCompleted(ctx context.Context, db *gorm.DB) (bool, error) {
var userCount int64
if err := db.WithContext(ctx).Model(&model.User{}).
Where("id != ?", staticApiKeyUserID).
Count(&userCount).Error; err != nil {
return false, err
}
return userCount != 0, nil
}
func (s *UserSignUpService) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]model.SignupToken, utils.PaginationResponse, error) {
var tokens []model.SignupToken
query := s.db.WithContext(ctx).Preload("UserGroups").Model(&model.SignupToken{})

View File

@@ -123,6 +123,10 @@ export default class UserService extends APIService {
return res.data as User;
};
checkInitialUserSetupAvailable = async () => {
await this.api.get('/signup/setup');
};
listSignupTokens = async (options?: ListRequestOptions) => {
const res = await this.api.get('/signup-tokens', { params: options });
return res.data as Paginated<SignupToken>;

View File

@@ -0,0 +1,18 @@
import { error } from '@sveltejs/kit';
import UserService from '$lib/services/user-service';
import { AxiosError } from 'axios';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
const userService = new UserService();
try {
await userService.checkInitialUserSetupAvailable();
} catch (e) {
if (e instanceof AxiosError && e.response?.status === 404) {
error(404, 'Not found');
}
throw e;
}
};

View File

@@ -81,15 +81,11 @@ test.describe('Initial User Signup', () => {
await expect(page.getByText('Set up your passkey')).toBeVisible();
});
test('Initial Signup - setup already completed', async ({ page }) => {
test('Initial Signup - setup route unavailable after completion', async ({ page }) => {
await cleanupBackend();
await page.goto('/setup');
await page.getByLabel('First name').fill('Test');
await page.getByLabel('Last name').fill('User');
await page.getByLabel('Username').fill('testuser123');
await page.getByLabel('Email').fill(users.tim.email);
await page.getByRole('button', { name: 'Sign Up' }).click();
await expect(page.getByText('Setup already completed')).toBeVisible();
await expect(page.getByText('Not found')).toBeVisible();
await expect(page.getByRole('button', { name: 'Sign Up' })).not.toBeVisible();
});
});