feat: disable registration
This commit is contained in:
parent
ab74d46c28
commit
39330ce821
17 changed files with 179 additions and 132 deletions
|
|
@ -12,6 +12,9 @@ JWT_SECRET=
|
||||||
# Go duration format
|
# Go duration format
|
||||||
JWT_EXPIRY=168h
|
JWT_EXPIRY=168h
|
||||||
|
|
||||||
|
# Set to true to block new account creation via magic link. Existing users can still log in.
|
||||||
|
DISABLE_REGISTRATION=false
|
||||||
|
|
||||||
MAILER_SMTP_HOST=
|
MAILER_SMTP_HOST=
|
||||||
MAILER_SMTP_PORT=
|
MAILER_SMTP_PORT=
|
||||||
MAILER_IMAP_HOST=
|
MAILER_IMAP_HOST=
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
UserService *service.UserService
|
UserService *service.UserService
|
||||||
AuthService *service.AuthService
|
AuthService *service.AuthService
|
||||||
EmailService *service.EmailService
|
EmailService *service.EmailService
|
||||||
SpaceService *service.SpaceService
|
SpaceService *service.SpaceService
|
||||||
AccountService *service.AccountService
|
AccountService *service.AccountService
|
||||||
AllocationService *service.AllocationService
|
AllocationService *service.AllocationService
|
||||||
TransactionService *service.TransactionService
|
TransactionService *service.TransactionService
|
||||||
RecurringEventService *service.RecurringEventService
|
RecurringEventService *service.RecurringEventService
|
||||||
InviteService *service.InviteService
|
InviteService *service.InviteService
|
||||||
AuditLogService *service.SpaceAuditLogService
|
AuditLogService *service.SpaceAuditLogService
|
||||||
TxAuditLogService *service.TransactionAuditLogService
|
TxAuditLogService *service.TransactionAuditLogService
|
||||||
AccountActivitySvc *service.AccountActivityService
|
AccountActivitySvc *service.AccountActivityService
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) (*App, error) {
|
func New(cfg *config.Config) (*App, error) {
|
||||||
|
|
@ -85,25 +85,26 @@ func New(cfg *config.Config) (*App, error) {
|
||||||
cfg.JWTExpiry,
|
cfg.JWTExpiry,
|
||||||
cfg.TokenMagicLinkExpiry,
|
cfg.TokenMagicLinkExpiry,
|
||||||
cfg.IsProduction(),
|
cfg.IsProduction(),
|
||||||
|
cfg.DisableRegistration,
|
||||||
)
|
)
|
||||||
inviteService := service.NewInviteService(invitationRepository, spaceRepository, userRepository, emailService, auditLogService)
|
inviteService := service.NewInviteService(invitationRepository, spaceRepository, userRepository, emailService, auditLogService)
|
||||||
recurringEventService := service.NewRecurringEventService(recurringEventRepository, transactionService, accountService)
|
recurringEventService := service.NewRecurringEventService(recurringEventRepository, transactionService, accountService)
|
||||||
|
|
||||||
return &App{
|
return &App{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
DB: database,
|
DB: database,
|
||||||
UserService: userService,
|
UserService: userService,
|
||||||
AuthService: authService,
|
AuthService: authService,
|
||||||
EmailService: emailService,
|
EmailService: emailService,
|
||||||
SpaceService: spaceService,
|
SpaceService: spaceService,
|
||||||
AccountService: accountService,
|
AccountService: accountService,
|
||||||
AllocationService: allocationService,
|
AllocationService: allocationService,
|
||||||
TransactionService: transactionService,
|
TransactionService: transactionService,
|
||||||
RecurringEventService: recurringEventService,
|
RecurringEventService: recurringEventService,
|
||||||
InviteService: inviteService,
|
InviteService: inviteService,
|
||||||
AuditLogService: auditLogService,
|
AuditLogService: auditLogService,
|
||||||
TxAuditLogService: txAuditLogService,
|
TxAuditLogService: txAuditLogService,
|
||||||
AccountActivitySvc: accountActivityService,
|
AccountActivitySvc: accountActivityService,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ type Config struct {
|
||||||
JWTExpiry time.Duration
|
JWTExpiry time.Duration
|
||||||
TokenMagicLinkExpiry time.Duration
|
TokenMagicLinkExpiry time.Duration
|
||||||
|
|
||||||
|
DisableRegistration bool
|
||||||
|
|
||||||
MailerSMTPHost string
|
MailerSMTPHost string
|
||||||
MailerSMTPPort int
|
MailerSMTPPort int
|
||||||
MailerIMAPHost string
|
MailerIMAPHost string
|
||||||
|
|
@ -58,6 +60,8 @@ func Load(version string) *Config {
|
||||||
JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default
|
JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default
|
||||||
TokenMagicLinkExpiry: envDuration("TOKEN_MAGIC_LINK_EXPIRY", 10*time.Minute),
|
TokenMagicLinkExpiry: envDuration("TOKEN_MAGIC_LINK_EXPIRY", 10*time.Minute),
|
||||||
|
|
||||||
|
DisableRegistration: envBool("DISABLE_REGISTRATION", false),
|
||||||
|
|
||||||
MailerSMTPHost: envString("MAILER_SMTP_HOST", ""),
|
MailerSMTPHost: envString("MAILER_SMTP_HOST", ""),
|
||||||
MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587),
|
MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587),
|
||||||
MailerIMAPHost: envString("MAILER_IMAP_HOST", ""),
|
MailerIMAPHost: envString("MAILER_IMAP_HOST", ""),
|
||||||
|
|
@ -120,6 +124,19 @@ func envInt(key string, def int) int {
|
||||||
return int(i)
|
return int(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envBool(key string, def bool) bool {
|
||||||
|
value, ok := os.LookupEnv(key)
|
||||||
|
if !ok || value == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
b, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("config invalid bool, using default", "key", key, "value", value, "default", def)
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func envDuration(key string, def time.Duration) time.Duration {
|
func envDuration(key string, def time.Duration) time.Duration {
|
||||||
value, ok := os.LookupEnv(key)
|
value, ok := os.LookupEnv(key)
|
||||||
if !ok || value == "" {
|
if !ok || value == "" {
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,8 @@ func (h *allocationHandler) renderSectionWithCreateError(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
ui.Render(w, r, blocks.AllocationsSection(blocks.AllocationsSectionProps{
|
ui.Render(w, r, blocks.AllocationsSection(blocks.AllocationsSectionProps{
|
||||||
SpaceID: spaceID, AccountID: accountID, Summary: summary,
|
SpaceID: spaceID, AccountID: accountID, Summary: summary,
|
||||||
CreateForm: &state,
|
CreateForm: &state,
|
||||||
ShowCreateForm: true,
|
ShowCreateForm: true,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,23 @@ func (h *authHandler) SendMagicLink(w http.ResponseWriter, r *http.Request) {
|
||||||
err = h.authService.SendMagicLink(email)
|
err = h.authService.SendMagicLink(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("magic link send failed", "error", err, "email", email)
|
slog.Warn("magic link send failed", "error", err, "email", email)
|
||||||
|
|
||||||
|
if errors.Is(err, service.ErrRegistrationDisabled) {
|
||||||
|
msg := "Registration is disabled. Please contact an administrator if you need an account."
|
||||||
|
if r.URL.Query().Get("resend") == "true" {
|
||||||
|
ui.RenderToast(w, r, toast.Toast(toast.Props{
|
||||||
|
Title: "Magic link not sent",
|
||||||
|
Description: msg,
|
||||||
|
Variant: toast.VariantError,
|
||||||
|
Icon: true,
|
||||||
|
Dismissible: true,
|
||||||
|
Duration: 5000,
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.Render(w, r, pages.Auth(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Query().Get("resend") == "true" {
|
if r.URL.Query().Get("resend") == "true" {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ func newTestAuthHandler(dbi testutil.DBInfo) *authHandler {
|
||||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||||
accountSvc := service.NewAccountService(accountRepo)
|
accountSvc := service.NewAccountService(accountRepo)
|
||||||
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||||
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false)
|
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false, false)
|
||||||
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil)
|
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil)
|
||||||
return NewAuthHandler(authSvc, inviteSvc, spaceSvc)
|
return NewAuthHandler(authSvc, inviteSvc, spaceSvc)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,22 +78,22 @@ func (h *recurringEventHandler) CreatePage(w http.ResponseWriter, r *http.Reques
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
formProps := forms.RecurringEventFormProps{
|
formProps := forms.RecurringEventFormProps{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
Action: routeurl.URL("action.app.spaces.space.recurring.create", "spaceID", spaceID),
|
Action: routeurl.URL("action.app.spaces.space.recurring.create", "spaceID", spaceID),
|
||||||
CancelHref: routeurl.URL("page.app.spaces.space.recurring", "spaceID", spaceID),
|
CancelHref: routeurl.URL("page.app.spaces.space.recurring", "spaceID", spaceID),
|
||||||
SubmitLabel: "Create",
|
SubmitLabel: "Create",
|
||||||
Accounts: accounts,
|
Accounts: accounts,
|
||||||
Timezones: timezone.CommonTimezones(),
|
Timezones: timezone.CommonTimezones(),
|
||||||
Kind: string(model.RecurringEventKindBill),
|
Kind: string(model.RecurringEventKindBill),
|
||||||
Frequency: string(model.RecurringFrequencyMonthly),
|
Frequency: string(model.RecurringFrequencyMonthly),
|
||||||
IntervalCount: "1",
|
IntervalCount: "1",
|
||||||
FireTime: "09:00",
|
FireTime: "09:00",
|
||||||
Timezone: "UTC",
|
Timezone: "UTC",
|
||||||
StartDate: now.Format("2006-01-02"),
|
StartDate: now.Format("2006-01-02"),
|
||||||
BusinessDaysOnly: false,
|
BusinessDaysOnly: false,
|
||||||
DayOfMonth: strconv.Itoa(now.Day()),
|
DayOfMonth: strconv.Itoa(now.Day()),
|
||||||
DayOfWeek: strconv.Itoa(int(now.Weekday())),
|
DayOfWeek: strconv.Itoa(int(now.Weekday())),
|
||||||
MonthOfYear: strconv.Itoa(int(now.Month())),
|
MonthOfYear: strconv.Itoa(int(now.Month())),
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Render(w, r, pages.SpaceCreateRecurringEventPage(pages.SpaceCreateRecurringEventPageProps{
|
ui.Render(w, r, pages.SpaceCreateRecurringEventPage(pages.SpaceCreateRecurringEventPageProps{
|
||||||
|
|
@ -126,19 +126,19 @@ func (h *recurringEventHandler) EditPage(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
formProps := forms.RecurringEventFormProps{
|
formProps := forms.RecurringEventFormProps{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
Action: routeurl.URL("action.app.spaces.space.recurring.event.edit", "spaceID", spaceID, "eventID", eventID),
|
Action: routeurl.URL("action.app.spaces.space.recurring.event.edit", "spaceID", spaceID, "eventID", eventID),
|
||||||
CancelHref: routeurl.URL("page.app.spaces.space.recurring", "spaceID", spaceID),
|
CancelHref: routeurl.URL("page.app.spaces.space.recurring", "spaceID", spaceID),
|
||||||
SubmitLabel: "Save",
|
SubmitLabel: "Save",
|
||||||
Accounts: accounts,
|
Accounts: accounts,
|
||||||
Timezones: timezone.CommonTimezones(),
|
Timezones: timezone.CommonTimezones(),
|
||||||
Title: ev.Title,
|
Title: ev.Title,
|
||||||
Kind: string(ev.Kind),
|
Kind: string(ev.Kind),
|
||||||
SourceAccountID: ev.SourceAccountID,
|
SourceAccountID: ev.SourceAccountID,
|
||||||
Amount: ev.Amount.StringFixedBank(2),
|
Amount: ev.Amount.StringFixedBank(2),
|
||||||
Frequency: string(ev.Frequency),
|
Frequency: string(ev.Frequency),
|
||||||
IntervalCount: strconv.Itoa(ev.IntervalCount),
|
IntervalCount: strconv.Itoa(ev.IntervalCount),
|
||||||
FireTime: formatTimeOfDay(ev.FireHour, ev.FireMinute),
|
FireTime: formatTimeOfDay(ev.FireHour, ev.FireMinute),
|
||||||
Timezone: ev.Timezone,
|
Timezone: ev.Timezone,
|
||||||
StartDate: ev.NextRunAt.In(mustLoc(ev.Timezone)).Format("2006-01-02"),
|
StartDate: ev.NextRunAt.In(mustLoc(ev.Timezone)).Format("2006-01-02"),
|
||||||
BusinessDaysOnly: ev.BusinessDaysOnly,
|
BusinessDaysOnly: ev.BusinessDaysOnly,
|
||||||
|
|
@ -215,19 +215,19 @@ func (h *recurringEventHandler) HandleEdit(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.recurringService.Update(service.UpdateRecurringEventInput{
|
if _, err := h.recurringService.Update(service.UpdateRecurringEventInput{
|
||||||
ID: eventID,
|
ID: eventID,
|
||||||
Kind: parsed.Kind,
|
Kind: parsed.Kind,
|
||||||
SourceAccountID: parsed.SourceAccountID,
|
SourceAccountID: parsed.SourceAccountID,
|
||||||
Title: parsed.Title,
|
Title: parsed.Title,
|
||||||
Amount: parsed.Amount,
|
Amount: parsed.Amount,
|
||||||
Description: parsed.Description,
|
Description: parsed.Description,
|
||||||
Frequency: parsed.Frequency,
|
Frequency: parsed.Frequency,
|
||||||
IntervalCount: parsed.IntervalCount,
|
IntervalCount: parsed.IntervalCount,
|
||||||
DayOfWeek: parsed.DayOfWeek,
|
DayOfWeek: parsed.DayOfWeek,
|
||||||
DayOfMonth: parsed.DayOfMonth,
|
DayOfMonth: parsed.DayOfMonth,
|
||||||
MonthOfYear: parsed.MonthOfYear,
|
MonthOfYear: parsed.MonthOfYear,
|
||||||
FireHour: parsed.FireHour,
|
FireHour: parsed.FireHour,
|
||||||
FireMinute: parsed.FireMinute,
|
FireMinute: parsed.FireMinute,
|
||||||
Timezone: parsed.Timezone,
|
Timezone: parsed.Timezone,
|
||||||
BusinessDaysOnly: parsed.BusinessDaysOnly,
|
BusinessDaysOnly: parsed.BusinessDaysOnly,
|
||||||
StartDate: parsed.StartDate,
|
StartDate: parsed.StartDate,
|
||||||
|
|
@ -305,19 +305,19 @@ func (h *recurringEventHandler) parseForm(r *http.Request, spaceID string) (serv
|
||||||
businessDaysOnly := r.FormValue("business_days_only") != ""
|
businessDaysOnly := r.FormValue("business_days_only") != ""
|
||||||
|
|
||||||
props := forms.RecurringEventFormProps{
|
props := forms.RecurringEventFormProps{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
Accounts: accounts,
|
Accounts: accounts,
|
||||||
Timezones: timezone.CommonTimezones(),
|
Timezones: timezone.CommonTimezones(),
|
||||||
Title: title,
|
Title: title,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
SourceAccountID: sourceID,
|
SourceAccountID: sourceID,
|
||||||
Amount: amountStr,
|
Amount: amountStr,
|
||||||
Description: descriptionStr,
|
Description: descriptionStr,
|
||||||
Frequency: frequency,
|
Frequency: frequency,
|
||||||
IntervalCount: intervalStr,
|
IntervalCount: intervalStr,
|
||||||
DayOfWeek: dowStr,
|
DayOfWeek: dowStr,
|
||||||
DayOfMonth: domStr,
|
DayOfMonth: domStr,
|
||||||
MonthOfYear: moyStr,
|
MonthOfYear: moyStr,
|
||||||
FireTime: fireTime,
|
FireTime: fireTime,
|
||||||
Timezone: tz,
|
Timezone: tz,
|
||||||
StartDate: startDateStr,
|
StartDate: startDateStr,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func newTestSettingsHandler(dbi testutil.DBInfo) (*settingsHandler, *service.Aut
|
||||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||||
accountSvc := service.NewAccountService(accountRepo)
|
accountSvc := service.NewAccountService(accountRepo)
|
||||||
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||||
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false)
|
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false, false)
|
||||||
userSvc := service.NewUserService(userRepo)
|
userSvc := service.NewUserService(userRepo)
|
||||||
return NewSettingsHandler(authSvc, userSvc), authSvc
|
return NewSettingsHandler(authSvc, userSvc), authSvc
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -330,15 +330,15 @@ func (h *spaceHandler) SpaceAccountPage(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Render(w, r, pages.SpaceAccountPage(pages.SpaceAccountPageProps{
|
ui.Render(w, r, pages.SpaceAccountPage(pages.SpaceAccountPageProps{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
SpaceName: space.Name,
|
SpaceName: space.Name,
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
AccountName: account.Name,
|
AccountName: account.Name,
|
||||||
AccountBalance: account.Balance,
|
AccountBalance: account.Balance,
|
||||||
AccountCurrency: account.Currency,
|
AccountCurrency: account.Currency,
|
||||||
RecentTransactions: recent,
|
RecentTransactions: recent,
|
||||||
NonEditableTransactionIDs: h.nonEditableTransactionIDs(recent),
|
NonEditableTransactionIDs: h.nonEditableTransactionIDs(recent),
|
||||||
AllocationSummary: allocSummary,
|
AllocationSummary: allocSummary,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,16 +396,16 @@ func (h *spaceHandler) SpaceAccountTransactionsPage(w http.ResponseWriter, r *ht
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Render(w, r, pages.SpaceAccountTransactionsPage(pages.SpaceAccountTransactionsPageProps{
|
ui.Render(w, r, pages.SpaceAccountTransactionsPage(pages.SpaceAccountTransactionsPageProps{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
SpaceName: space.Name,
|
SpaceName: space.Name,
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
AccountName: account.Name,
|
AccountName: account.Name,
|
||||||
Transactions: txns,
|
Transactions: txns,
|
||||||
NonEditableTransactionIDs: h.nonEditableTransactionIDs(txns),
|
NonEditableTransactionIDs: h.nonEditableTransactionIDs(txns),
|
||||||
CurrentPage: page,
|
CurrentPage: page,
|
||||||
TotalPages: totalPages,
|
TotalPages: totalPages,
|
||||||
TotalCount: total,
|
TotalCount: total,
|
||||||
PerPage: perPage,
|
PerPage: perPage,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,19 @@ import "time"
|
||||||
type SpaceAuditAction string
|
type SpaceAuditAction string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SpaceAuditActionRenamed SpaceAuditAction = "space.renamed"
|
SpaceAuditActionRenamed SpaceAuditAction = "space.renamed"
|
||||||
SpaceAuditActionDeleted SpaceAuditAction = "space.deleted"
|
SpaceAuditActionDeleted SpaceAuditAction = "space.deleted"
|
||||||
SpaceAuditActionMemberInvited SpaceAuditAction = "member.invited"
|
SpaceAuditActionMemberInvited SpaceAuditAction = "member.invited"
|
||||||
SpaceAuditActionMemberJoined SpaceAuditAction = "member.joined"
|
SpaceAuditActionMemberJoined SpaceAuditAction = "member.joined"
|
||||||
SpaceAuditActionMemberRemoved SpaceAuditAction = "member.removed"
|
SpaceAuditActionMemberRemoved SpaceAuditAction = "member.removed"
|
||||||
SpaceAuditActionInviteCancelled SpaceAuditAction = "invite.cancelled"
|
SpaceAuditActionInviteCancelled SpaceAuditAction = "invite.cancelled"
|
||||||
SpaceAuditActionAccountCreated SpaceAuditAction = "account.created"
|
SpaceAuditActionAccountCreated SpaceAuditAction = "account.created"
|
||||||
SpaceAuditActionAccountRenamed SpaceAuditAction = "account.renamed"
|
SpaceAuditActionAccountRenamed SpaceAuditAction = "account.renamed"
|
||||||
SpaceAuditActionAccountDeleted SpaceAuditAction = "account.deleted"
|
SpaceAuditActionAccountDeleted SpaceAuditAction = "account.deleted"
|
||||||
SpaceAuditActionAccountCurrencyChanged SpaceAuditAction = "account.currency_changed"
|
SpaceAuditActionAccountCurrencyChanged SpaceAuditAction = "account.currency_changed"
|
||||||
SpaceAuditActionAllocationCreated SpaceAuditAction = "allocation.created"
|
SpaceAuditActionAllocationCreated SpaceAuditAction = "allocation.created"
|
||||||
SpaceAuditActionAllocationUpdated SpaceAuditAction = "allocation.updated"
|
SpaceAuditActionAllocationUpdated SpaceAuditAction = "allocation.updated"
|
||||||
SpaceAuditActionAllocationDeleted SpaceAuditAction = "allocation.deleted"
|
SpaceAuditActionAllocationDeleted SpaceAuditAction = "allocation.deleted"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SpaceAuditLog struct {
|
type SpaceAuditLog struct {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func newTestApp(dbi testutil.DBInfo) *app.App {
|
||||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||||
accountSvc := service.NewAccountService(accountRepo)
|
accountSvc := service.NewAccountService(accountRepo)
|
||||||
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||||
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false)
|
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false, false)
|
||||||
userSvc := service.NewUserService(userRepo)
|
userSvc := service.NewUserService(userRepo)
|
||||||
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil)
|
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func (s *stubSpaceAuditRepo) ListBySpace(_ string, limit, _ int) ([]*model.Space
|
||||||
}
|
}
|
||||||
return firstN(s.listSpace, limit), nil
|
return firstN(s.listSpace, limit), nil
|
||||||
}
|
}
|
||||||
func (s *stubSpaceAuditRepo) CountBySpace(string) (int, error) { return s.countSpace, s.err }
|
func (s *stubSpaceAuditRepo) CountBySpace(string) (int, error) { return s.countSpace, s.err }
|
||||||
func (s *stubSpaceAuditRepo) ListAccountEvents(_ string, limit, _ int) ([]*model.SpaceAuditLogWithActor, error) {
|
func (s *stubSpaceAuditRepo) ListAccountEvents(_ string, limit, _ int) ([]*model.SpaceAuditLogWithActor, error) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return nil, s.err
|
return nil, s.err
|
||||||
|
|
@ -99,9 +99,9 @@ func TestAccountActivityService_List_MergesAndSortsByTimestamp(t *testing.T) {
|
||||||
}
|
}
|
||||||
txRepo := &stubTxAuditRepo{
|
txRepo := &stubTxAuditRepo{
|
||||||
listAccount: []*model.TransactionAuditLogWithActor{
|
listAccount: []*model.TransactionAuditLogWithActor{
|
||||||
txLog(model.TransactionAuditActionEdited, now), // newest overall
|
txLog(model.TransactionAuditActionEdited, now), // newest overall
|
||||||
txLog(model.TransactionAuditActionCreated, now.Add(-5*time.Minute)),
|
txLog(model.TransactionAuditActionCreated, now.Add(-5*time.Minute)),
|
||||||
txLog(model.TransactionAuditActionDeleted, now.Add(-15*time.Minute)), // oldest overall
|
txLog(model.TransactionAuditActionDeleted, now.Add(-15*time.Minute)), // oldest overall
|
||||||
},
|
},
|
||||||
countAccount: 3,
|
countAccount: 3,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidCredentials = errors.New("invalid email or password")
|
ErrInvalidCredentials = errors.New("invalid email or password")
|
||||||
ErrNoPassword = errors.New("account uses passwordless login. Use magic link")
|
ErrNoPassword = errors.New("account uses passwordless login. Use magic link")
|
||||||
ErrPasswordsDoNotMatch = errors.New("passwords do not match")
|
ErrPasswordsDoNotMatch = errors.New("passwords do not match")
|
||||||
ErrEmailAlreadyExists = errors.New("email already exists")
|
ErrEmailAlreadyExists = errors.New("email already exists")
|
||||||
ErrWeakPassword = errors.New("password must be at least 12 characters")
|
ErrWeakPassword = errors.New("password must be at least 12 characters")
|
||||||
ErrCommonPassword = errors.New("password is too common, please choose a stronger one")
|
ErrCommonPassword = errors.New("password is too common, please choose a stronger one")
|
||||||
ErrEmailNotVerified = errors.New("email not verified")
|
ErrEmailNotVerified = errors.New("email not verified")
|
||||||
ErrInvalidEmail = errors.New("invalid email address")
|
ErrInvalidEmail = errors.New("invalid email address")
|
||||||
ErrNameRequired = errors.New("name is required")
|
ErrNameRequired = errors.New("name is required")
|
||||||
|
ErrRegistrationDisabled = errors.New("registration is disabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
|
|
@ -41,6 +42,7 @@ type AuthService struct {
|
||||||
jwtExpiry time.Duration
|
jwtExpiry time.Duration
|
||||||
tokenMagicLinkExpiry time.Duration
|
tokenMagicLinkExpiry time.Duration
|
||||||
isProduction bool
|
isProduction bool
|
||||||
|
disableRegistration bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(
|
func NewAuthService(
|
||||||
|
|
@ -53,6 +55,7 @@ func NewAuthService(
|
||||||
jwtExpiry time.Duration,
|
jwtExpiry time.Duration,
|
||||||
tokenMagicLinkExpiry time.Duration,
|
tokenMagicLinkExpiry time.Duration,
|
||||||
isProduction bool,
|
isProduction bool,
|
||||||
|
disableRegistration bool,
|
||||||
) *AuthService {
|
) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
|
|
@ -64,6 +67,7 @@ func NewAuthService(
|
||||||
jwtExpiry: jwtExpiry,
|
jwtExpiry: jwtExpiry,
|
||||||
tokenMagicLinkExpiry: tokenMagicLinkExpiry,
|
tokenMagicLinkExpiry: tokenMagicLinkExpiry,
|
||||||
isProduction: isProduction,
|
isProduction: isProduction,
|
||||||
|
disableRegistration: disableRegistration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,6 +239,10 @@ func (s *AuthService) SendMagicLink(email string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// User doesn't exist - create a new passwordless account
|
// User doesn't exist - create a new passwordless account
|
||||||
if errors.Is(err, repository.ErrUserNotFound) {
|
if errors.Is(err, repository.ErrUserNotFound) {
|
||||||
|
if s.disableRegistration {
|
||||||
|
slog.Info("registration disabled, refusing to create new user", "email", email)
|
||||||
|
return ErrRegistrationDisabled
|
||||||
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
user = &model.User{
|
user = &model.User{
|
||||||
ID: uuid.NewString(),
|
ID: uuid.NewString(),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ func newTestAuthService(dbi testutil.DBInfo) *AuthService {
|
||||||
cfg.JWTExpiry,
|
cfg.JWTExpiry,
|
||||||
cfg.TokenMagicLinkExpiry,
|
cfg.TokenMagicLinkExpiry,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,9 @@ func TestFirstFireOnOrAfter_WeeklyShiftsToTargetDayOfWeek(t *testing.T) {
|
||||||
|
|
||||||
func TestAddMonths(t *testing.T) {
|
func TestAddMonths(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
y int
|
y int
|
||||||
m time.Month
|
m time.Month
|
||||||
n int
|
n int
|
||||||
wy int
|
wy int
|
||||||
wm time.Month
|
wm time.Month
|
||||||
}{
|
}{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeSpaceAuditRepo struct {
|
type fakeSpaceAuditRepo struct {
|
||||||
created []*model.SpaceAuditLog
|
created []*model.SpaceAuditLog
|
||||||
failNext error
|
failNext error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ import (
|
||||||
// txnFixture builds a fully wired TransactionService against a real DB along with
|
// txnFixture builds a fully wired TransactionService against a real DB along with
|
||||||
// the helper repos the tests need to inspect post-state.
|
// the helper repos the tests need to inspect post-state.
|
||||||
type txnFixture struct {
|
type txnFixture struct {
|
||||||
svc *TransactionService
|
svc *TransactionService
|
||||||
txAudit repository.TransactionAuditLogRepository
|
txAudit repository.TransactionAuditLogRepository
|
||||||
accounts repository.AccountRepository
|
accounts repository.AccountRepository
|
||||||
user *model.User
|
user *model.User
|
||||||
account *model.Account
|
account *model.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTxnFixture(t *testing.T, dbi testutil.DBInfo) *txnFixture {
|
func newTxnFixture(t *testing.T, dbi testutil.DBInfo) *txnFixture {
|
||||||
|
|
@ -149,8 +149,8 @@ func TestTransactionService_UpdateDeposit_RebalancesAndDiffs(t *testing.T) {
|
||||||
assert.Equal(t, model.TransactionAuditActionEdited, logs[0].Action)
|
assert.Equal(t, model.TransactionAuditActionEdited, logs[0].Action)
|
||||||
|
|
||||||
var meta struct {
|
var meta struct {
|
||||||
AccountID string `json:"account_id"`
|
AccountID string `json:"account_id"`
|
||||||
Changes map[string]map[string]any `json:"changes"`
|
Changes map[string]map[string]any `json:"changes"`
|
||||||
}
|
}
|
||||||
require.NoError(t, json.Unmarshal(logs[0].Metadata, &meta))
|
require.NoError(t, json.Unmarshal(logs[0].Metadata, &meta))
|
||||||
assert.Equal(t, f.account.ID, meta.AccountID)
|
assert.Equal(t, f.account.ID, meta.AccountID)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue