diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index 7062d2c0..cab2ebb2 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -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{} diff --git a/backend/internal/controller/user_signup_controller.go b/backend/internal/controller/user_signup_controller.go index 4b53c44f..b8c4edf5 100644 --- a/backend/internal/controller/user_signup_controller.go +++ b/backend/internal/controller/user_signup_controller.go @@ -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 diff --git a/backend/internal/service/user_signup_service.go b/backend/internal/service/user_signup_service.go index 222e1a01..b733762e 100644 --- a/backend/internal/service/user_signup_service.go +++ b/backend/internal/service/user_signup_service.go @@ -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{}) diff --git a/frontend/src/lib/services/user-service.ts b/frontend/src/lib/services/user-service.ts index c7d69274..230ecdd3 100644 --- a/frontend/src/lib/services/user-service.ts +++ b/frontend/src/lib/services/user-service.ts @@ -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; diff --git a/frontend/src/routes/signup/setup/+page.ts b/frontend/src/routes/signup/setup/+page.ts new file mode 100644 index 00000000..9be70df0 --- /dev/null +++ b/frontend/src/routes/signup/setup/+page.ts @@ -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; + } +}; diff --git a/tests/specs/user-signup.spec.ts b/tests/specs/user-signup.spec.ts index 8cb48b30..e178f502 100644 --- a/tests/specs/user-signup.spec.ts +++ b/tests/specs/user-signup.spec.ts @@ -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(); }); });