mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-18 16:59:53 +00:00
234 lines
6.1 KiB
Go
234 lines
6.1 KiB
Go
package usersignup
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"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/model"
|
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
|
)
|
|
|
|
// authenticationMethodOneTimePassword identifies one-time-password authentication, used for the initial admin setup token
|
|
// It must match the value emitted by the JWT service in the access token's "amr" claim
|
|
const authenticationMethodOneTimePassword = "otp"
|
|
|
|
type Service struct {
|
|
db *gorm.DB
|
|
userCreator UserCreator
|
|
signer TokenService
|
|
auditLog AuditLogger
|
|
appConfig AppConfigProvider
|
|
}
|
|
|
|
func newService(deps Dependencies) *Service {
|
|
return &Service{
|
|
db: deps.DB,
|
|
userCreator: deps.UserCreator,
|
|
signer: deps.Signer,
|
|
auditLog: deps.AuditLog,
|
|
appConfig: deps.AppConfig,
|
|
}
|
|
}
|
|
|
|
func (s *Service) SignUp(ctx context.Context, signupData signUpDto, ipAddress, userAgent string) (model.User, string, error) {
|
|
tx := s.db.Begin()
|
|
defer func() {
|
|
tx.Rollback()
|
|
}()
|
|
|
|
tokenProvided := signupData.Token != ""
|
|
|
|
config := s.appConfig.GetDbConfig()
|
|
if config.AllowUserSignups.Value != "open" && !tokenProvided {
|
|
return model.User{}, "", &common.OpenSignupDisabledError{}
|
|
}
|
|
|
|
var signupToken SignupToken
|
|
var userGroupIDs []string
|
|
if tokenProvided {
|
|
err := tx.
|
|
WithContext(ctx).
|
|
Preload("UserGroups").
|
|
Where("token = ?", signupData.Token).
|
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
First(&signupToken).
|
|
Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return model.User{}, "", &common.TokenInvalidOrExpiredError{}
|
|
}
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
if !signupToken.IsValid() {
|
|
return model.User{}, "", &common.TokenInvalidOrExpiredError{}
|
|
}
|
|
|
|
for _, group := range signupToken.UserGroups {
|
|
userGroupIDs = append(userGroupIDs, group.ID)
|
|
}
|
|
}
|
|
|
|
userToCreate := dto.UserCreateDto{
|
|
Username: signupData.Username,
|
|
Email: signupData.Email,
|
|
FirstName: signupData.FirstName,
|
|
LastName: signupData.LastName,
|
|
DisplayName: strings.TrimSpace(signupData.FirstName + " " + signupData.LastName),
|
|
UserGroupIds: userGroupIDs,
|
|
EmailVerified: s.appConfig.GetDbConfig().EmailsVerified.IsTrue(),
|
|
}
|
|
|
|
user, err := s.userCreator.CreateUserInternal(ctx, userToCreate, false, tx)
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
accessToken, err := s.signer.GenerateAccessToken(user, "")
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
if tokenProvided {
|
|
s.auditLog.Create(ctx, model.AuditLogEventAccountCreated, ipAddress, userAgent, user.ID, model.AuditLogData{
|
|
"signupToken": signupToken.Token,
|
|
}, tx)
|
|
|
|
signupToken.UsageCount++
|
|
|
|
err = tx.WithContext(ctx).Save(&signupToken).Error
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
} else {
|
|
s.auditLog.Create(ctx, model.AuditLogEventAccountCreated, ipAddress, userAgent, user.ID, model.AuditLogData{
|
|
"method": "open_signup",
|
|
}, tx)
|
|
}
|
|
|
|
err = tx.Commit().Error
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
return user, accessToken, nil
|
|
}
|
|
|
|
func (s *Service) SignUpInitialAdmin(ctx context.Context, signUpData signUpDto) (model.User, string, error) {
|
|
tx := s.db.Begin()
|
|
defer func() {
|
|
tx.Rollback()
|
|
}()
|
|
|
|
setupCompleted, err := s.isInitialAdminSetupCompleted(ctx, tx)
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
if setupCompleted {
|
|
return model.User{}, "", &common.SetupNotAvailableError{}
|
|
}
|
|
|
|
userToCreate := dto.UserCreateDto{
|
|
FirstName: signUpData.FirstName,
|
|
LastName: signUpData.LastName,
|
|
DisplayName: strings.TrimSpace(signUpData.FirstName + " " + signUpData.LastName),
|
|
Username: signUpData.Username,
|
|
Email: signUpData.Email,
|
|
IsAdmin: true,
|
|
}
|
|
|
|
user, err := s.userCreator.CreateUserInternal(ctx, userToCreate, false, tx)
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
token, err := s.signer.GenerateAccessToken(user, authenticationMethodOneTimePassword)
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
err = tx.Commit().Error
|
|
if err != nil {
|
|
return model.User{}, "", err
|
|
}
|
|
|
|
return user, token, nil
|
|
}
|
|
|
|
func (s *Service) IsInitialAdminSetupCompleted(ctx context.Context) (bool, error) {
|
|
return s.isInitialAdminSetupCompleted(ctx, s.db)
|
|
}
|
|
|
|
func (s *Service) isInitialAdminSetupCompleted(ctx context.Context, db *gorm.DB) (bool, error) {
|
|
var userCount int64
|
|
if err := db.WithContext(ctx).Model(&model.User{}).
|
|
Where("id != ?", common.StaticApiKeyUserID).
|
|
Count(&userCount).Error; err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return userCount != 0, nil
|
|
}
|
|
|
|
func (s *Service) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]SignupToken, utils.PaginationResponse, error) {
|
|
var tokens []SignupToken
|
|
query := s.db.WithContext(ctx).Preload("UserGroups").Model(&SignupToken{})
|
|
|
|
pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &tokens)
|
|
return tokens, pagination, err
|
|
}
|
|
|
|
func (s *Service) DeleteSignupToken(ctx context.Context, tokenID string) error {
|
|
return s.db.WithContext(ctx).Delete(&SignupToken{}, "id = ?", tokenID).Error
|
|
}
|
|
|
|
func (s *Service) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int, userGroupIDs []string) (SignupToken, error) {
|
|
signupToken, err := newSignupToken(ttl, usageLimit)
|
|
if err != nil {
|
|
return SignupToken{}, err
|
|
}
|
|
|
|
var userGroups []model.UserGroup
|
|
err = s.db.WithContext(ctx).
|
|
Where("id IN ?", userGroupIDs).
|
|
Find(&userGroups).
|
|
Error
|
|
if err != nil {
|
|
return SignupToken{}, err
|
|
}
|
|
signupToken.UserGroups = userGroups
|
|
|
|
err = s.db.WithContext(ctx).Create(signupToken).Error
|
|
if err != nil {
|
|
return SignupToken{}, err
|
|
}
|
|
|
|
return *signupToken, nil
|
|
}
|
|
|
|
func newSignupToken(ttl time.Duration, usageLimit int) (*SignupToken, error) {
|
|
// Generate a random token
|
|
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now().Round(time.Second)
|
|
token := &SignupToken{
|
|
Token: randomString,
|
|
ExpiresAt: datatype.DateTime(now.Add(ttl)),
|
|
UsageLimit: usageLimit,
|
|
UsageCount: 0,
|
|
}
|
|
|
|
return token, nil
|
|
}
|