feat(auth): add staged account request validation and admin review workflow
This commit is contained in:
@@ -58,3 +58,9 @@ Project Change ID[date-time] - application-version - Type - Summary:
|
||||
- 1 - I added `ShouldEnableNewAccountRequestNotificationsFallback` in `models/user/setting_options.go` so the code can detect when the deleted user is the last active admin with this notification enabled and the acting admin is currently unsubscribed.
|
||||
- 2 - I modified `routers/web/admin/users.go` so, after a successful admin-driven user deletion, the acting admin is automatically subscribed to new account request notifications when they deleted the last subscribed admin.
|
||||
- 3 - I modified `routers/api/v1/admin/user.go` so the same automatic fallback also applies to admin deletions performed through the API, keeping the behavior consistent across both deletion entry points.
|
||||
|
||||
10 - [2026-04-16 23:02:55] - v.1.27.0-dev-42-g81727dd3e9 - Type: Added - Implemented a staged new account request workflow with email validation, admin review, request statuses, and automatic expiry cleanup.
|
||||
- 1 - I added `models/user/account_request.go` to store and manage account request states, validation expiry, retry counting, approval or rejection metadata, validation code generation, and expired pending-request cleanup.
|
||||
- 2 - I modified `routers/web/auth/auth.go`, added `routers/web/auth/account_request.go`, and updated `templates/user/auth/signup_inner.tmpl` so registration now creates pending account requests, resends validation mail when appropriate, blocks repeated unconfirmed attempts after five tries, and moves validated requests into administrator review instead of activating them immediately.
|
||||
- 3 - I modified `routers/web/admin/users.go`, added `routers/web/admin/account_request.go`, updated `routers/web/web.go`, and updated `templates/admin/user/edit.tmpl` so administrators can see the account request status for a user and explicitly activate, reject, or unblock requests from the admin user edit page.
|
||||
- 4 - I modified `services/mailer/mail_user.go`, added the new account request mail templates, updated `options/locale/locale_en-US.json`, and modified `services/user/user.go` with `services/cron/tasks_extended.go` so the workflow now sends dedicated validation, approval, and rejection emails, preserves rejected or blocked accounts from inactive-user cleanup, and automatically deletes only expired requests that never completed email validation.
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
SettingsKeyAccountRequestStatus = "account_request.status"
|
||||
SettingsKeyAccountRequestExpiresUnix = "account_request.expires_unix"
|
||||
SettingsKeyAccountRequestSignupAttempts = "account_request.signup_attempts"
|
||||
SettingsKeyAccountRequestEmailValidatedUnix = "account_request.email_validated_unix"
|
||||
SettingsKeyAccountRequestReviewedUnix = "account_request.reviewed_unix"
|
||||
SettingsKeyAccountRequestReviewedBy = "account_request.reviewed_by"
|
||||
|
||||
AccountRequestStatusPendingEmailValidation = "pending_email_validation"
|
||||
AccountRequestStatusPendingAdminReview = "pending_admin_review"
|
||||
AccountRequestStatusRejected = "rejected"
|
||||
AccountRequestStatusBlocked = "blocked"
|
||||
|
||||
AccountRequestValidationHours = 48
|
||||
AccountRequestMaxAttempts = 5
|
||||
)
|
||||
|
||||
func GetAccountRequestStatus(ctx context.Context, user *User) (string, error) {
|
||||
return GetUserSetting(ctx, user.ID, SettingsKeyAccountRequestStatus)
|
||||
}
|
||||
|
||||
func SetAccountRequestStatus(ctx context.Context, userID int64, status string) error {
|
||||
if status == "" {
|
||||
return DeleteUserSetting(ctx, userID, SettingsKeyAccountRequestStatus)
|
||||
}
|
||||
return SetUserSetting(ctx, userID, SettingsKeyAccountRequestStatus, status)
|
||||
}
|
||||
|
||||
func getAccountRequestInt64(ctx context.Context, userID int64, key string) (int64, error) {
|
||||
value, err := GetUserSetting(ctx, userID, key)
|
||||
if err != nil || value == "" {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
|
||||
func setAccountRequestInt64(ctx context.Context, userID int64, key string, value int64) error {
|
||||
if value == 0 {
|
||||
return DeleteUserSetting(ctx, userID, key)
|
||||
}
|
||||
return SetUserSetting(ctx, userID, key, strconv.FormatInt(value, 10))
|
||||
}
|
||||
|
||||
func GetAccountRequestExpiry(ctx context.Context, user *User) (time.Time, error) {
|
||||
unix, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix)
|
||||
if err != nil || unix == 0 {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Unix(unix, 0), nil
|
||||
}
|
||||
|
||||
func RefreshAccountRequestValidationWindow(ctx context.Context, user *User) error {
|
||||
return setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix, time.Now().Add(AccountRequestValidationHours*time.Hour).Unix())
|
||||
}
|
||||
|
||||
func GetAccountRequestSignupAttempts(ctx context.Context, user *User) (int, error) {
|
||||
value, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestSignupAttempts)
|
||||
return int(value), err
|
||||
}
|
||||
|
||||
func SetAccountRequestSignupAttempts(ctx context.Context, userID int64, attempts int) error {
|
||||
return setAccountRequestInt64(ctx, userID, SettingsKeyAccountRequestSignupAttempts, int64(attempts))
|
||||
}
|
||||
|
||||
func IsAccountRequestExpired(ctx context.Context, user *User) (bool, error) {
|
||||
status, err := GetAccountRequestStatus(ctx, user)
|
||||
if err != nil || status != AccountRequestStatusPendingEmailValidation {
|
||||
return false, err
|
||||
}
|
||||
expiresAt, err := GetAccountRequestExpiry(ctx, user)
|
||||
if err != nil || expiresAt.IsZero() {
|
||||
return false, err
|
||||
}
|
||||
return time.Now().After(expiresAt), nil
|
||||
}
|
||||
|
||||
func ActivateAccountRequestPrimaryEmail(ctx context.Context, user *User) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
addr, exist, err := db.Get[EmailAddress](ctx, builder.Eq{"uid": user.ID, "lower_email": strings.ToLower(user.Email)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return fmt.Errorf("no such email: %d (%s)", user.ID, user.Email)
|
||||
}
|
||||
if addr.IsActivated {
|
||||
return nil
|
||||
}
|
||||
return updateActivation(ctx, addr, true)
|
||||
})
|
||||
}
|
||||
|
||||
func ResetAccountRequestToPendingEmailValidation(ctx context.Context, user *User) error {
|
||||
if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusPendingEmailValidation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := RefreshAccountRequestValidationWindow(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SetAccountRequestSignupAttempts(ctx, user.ID, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestEmailValidatedUnix)
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestReviewedUnix)
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestReviewedBy)
|
||||
user.ProhibitLogin = false
|
||||
_, err := db.GetEngine(ctx).ID(user.ID).Cols("prohibit_login").Update(user)
|
||||
return err
|
||||
}
|
||||
|
||||
func IncrementAccountRequestSignupAttempts(ctx context.Context, user *User) (attempts int, blocked bool, _ error) {
|
||||
attempts, err := GetAccountRequestSignupAttempts(ctx, user)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
attempts++
|
||||
if err := SetAccountRequestSignupAttempts(ctx, user.ID, attempts); err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if attempts < AccountRequestMaxAttempts {
|
||||
return attempts, false, nil
|
||||
}
|
||||
if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusBlocked); err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
user.ProhibitLogin = true
|
||||
_, err = db.GetEngine(ctx).ID(user.ID).Cols("prohibit_login").Update(user)
|
||||
return attempts, true, err
|
||||
}
|
||||
|
||||
func MarkAccountRequestPendingAdminReview(ctx context.Context, user *User) error {
|
||||
if err := ActivateAccountRequestPrimaryEmail(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusPendingAdminReview); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestEmailValidatedUnix, time.Now().Unix()); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RejectAccountRequest(ctx context.Context, user *User, reviewerID int64) error {
|
||||
if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusRejected); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedUnix, time.Now().Unix()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedBy, reviewerID); err != nil {
|
||||
return err
|
||||
}
|
||||
user.ProhibitLogin = true
|
||||
_, err := db.GetEngine(ctx).ID(user.ID).Cols("prohibit_login").Update(user)
|
||||
return err
|
||||
}
|
||||
|
||||
func ApproveAccountRequest(ctx context.Context, user *User, reviewerID int64) error {
|
||||
if err := ActivateAccountRequestPrimaryEmail(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
user.IsActive = true
|
||||
user.ProhibitLogin = false
|
||||
if user.Rands, err = GetUserSalt(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := UpdateUserCols(ctx, user, "is_active", "prohibit_login", "rands"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedUnix, time.Now().Unix()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedBy, reviewerID); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestStatus)
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix)
|
||||
_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestSignupAttempts)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnblockAccountRequest(ctx context.Context, user *User) error {
|
||||
return ResetAccountRequestToPendingEmailValidation(ctx, user)
|
||||
}
|
||||
|
||||
func GenerateAccountRequestValidationCode(user *User) string {
|
||||
data := makeTimeLimitCodeHashData(&TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateAccount}, user)
|
||||
code := base.CreateTimeLimitCode(data, AccountRequestValidationHours*60, time.Now(), nil)
|
||||
code += fmt.Sprintf("%x", []byte(user.LowerName))
|
||||
return code
|
||||
}
|
||||
|
||||
func VerifyAccountRequestValidationCode(ctx context.Context, code string) (user *User) {
|
||||
if user = GetVerifyUser(ctx, code); user != nil {
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := makeTimeLimitCodeHashData(&TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateAccount}, user)
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, AccountRequestValidationHours*60, prefix) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUserByAnyEmail(ctx context.Context, email string) (*User, error) {
|
||||
if len(email) == 0 {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
addr := &EmailAddress{LowerEmail: strings.ToLower(strings.TrimSpace(email))}
|
||||
has, err := db.GetEngine(ctx).Get(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
return GetUserByID(ctx, addr.UID)
|
||||
}
|
||||
|
||||
func DeleteExpiredPendingAccountRequests(ctx context.Context) error {
|
||||
users, _, err := SearchUsers(ctx, SearchUserOptions{
|
||||
Types: []UserType{UserTypeIndividual, UserTypeUserReserved},
|
||||
IsActive: optional.Some(false),
|
||||
ListOptions: db.ListOptionsAll,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range users {
|
||||
status, err := GetAccountRequestStatus(ctx, user)
|
||||
if err != nil || status != AccountRequestStatusPendingEmailValidation {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
expired, err := IsAccountRequestExpired(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !expired {
|
||||
continue
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Where("uid=?", user.ID).Delete(&EmailAddress{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Where("uid=?", user.ID).Delete(&Setting{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).ID(user.ID).Delete(&User{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -379,6 +379,16 @@
|
||||
"auth.disable_register_prompt": "Registration is disabled. Please contact your site administrator.",
|
||||
"auth.disable_register_mail": "Email confirmation for registration is disabled.",
|
||||
"auth.manual_activation_only": "Contact your site administrator to complete activation.",
|
||||
"auth.account_request_validation_sent": "We sent you a validation email for this account request. The link is valid for 48 hours.",
|
||||
"auth.account_request_validation_pending_signup": "A validation email has already been sent for this account request. Please validate it before creating another account.",
|
||||
"auth.account_request_validation_resent": "We sent a new validation email for this account request.",
|
||||
"auth.account_request_validation_expired": "This account request validation link has expired. Please register again.",
|
||||
"auth.account_request_admin_review_pending": "Your email address has been validated. An administrator will now review your account request.",
|
||||
"auth.account_request_validated_waiting_admin": "Your account request was validated successfully. Please wait for an administrator to review it.",
|
||||
"auth.account_request_resend": "Did not receive the email? Resend",
|
||||
"auth.account_request_resend_not_available": "This account request can no longer receive a validation email.",
|
||||
"auth.account_request_blocked": "This account request has been blocked after %d unconfirmed attempts. An administrator must unblock it.",
|
||||
"auth.account_request_rejected_signup": "This account request was rejected. An administrator must unblock it before the same email address can be used again.",
|
||||
"auth.remember_me": "Remember This Device",
|
||||
"auth.remember_me.compromised": "The login token is not valid anymore which may indicate a compromised account. Please check your account for unusual activities.",
|
||||
"auth.forgot_password_title": "Forgot Password",
|
||||
@@ -467,6 +477,18 @@
|
||||
"mail.new_account_request.text_1": "A new user account request is awaiting administrator review on %s.",
|
||||
"mail.new_account_request.text_2": "Requested account: <b>%[1]s</b> (%[2]s).",
|
||||
"mail.new_account_request.text_3": "Review and activate the account here:",
|
||||
"mail.account_request.validate": "Validate your account request",
|
||||
"mail.account_request.validate.title": "%s, validate your account request",
|
||||
"mail.account_request.validate.text_1": "Hi <b>%[1]s</b>, please validate your account request for %[2]s.",
|
||||
"mail.account_request.validate.text_2": "Click the following link within <b>%s</b> to validate your email address and submit the request for administrator review:",
|
||||
"mail.account_request.approved": "Your account request for %s was approved",
|
||||
"mail.account_request.approved.title": "Your account request for %s was approved",
|
||||
"mail.account_request.approved.text_1": "Hi <b>%[1]s</b>, your account request for %[2]s was approved.",
|
||||
"mail.account_request.approved.text_2": "You can now sign in with your account.",
|
||||
"mail.account_request.rejected": "Your account request for %s was rejected",
|
||||
"mail.account_request.rejected.title": "Your account request for %s was rejected",
|
||||
"mail.account_request.rejected.text_1": "Hi <b>%[1]s</b>, your account request for %[2]s was rejected.",
|
||||
"mail.account_request.rejected.text_2": "The account remains blocked until an administrator unblocks it.",
|
||||
"mail.register_notify": "Welcome to %s",
|
||||
"mail.register_notify.title": "%[1]s, welcome to %[2]s",
|
||||
"mail.register_notify.text_1": "This is your registration confirmation email for %s!",
|
||||
@@ -3051,6 +3073,20 @@
|
||||
"admin.users.purge_help": "Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments will be deleted too.",
|
||||
"admin.users.still_own_packages": "This user still owns one or more packages. Delete these packages first.",
|
||||
"admin.users.deletion_success": "The user account has been deleted.",
|
||||
"admin.users.account_request.status": "Account request status",
|
||||
"admin.users.account_request.attempts": "Unconfirmed attempts: %d",
|
||||
"admin.users.account_request.activate": "Activate Request",
|
||||
"admin.users.account_request.reject": "Reject Request",
|
||||
"admin.users.account_request.unblock": "Unblock Request",
|
||||
"admin.users.account_request.approved": "The account request has been approved.",
|
||||
"admin.users.account_request.rejected": "The account request has been rejected.",
|
||||
"admin.users.account_request.unblocked": "The account request has been unblocked and a new validation email was sent.",
|
||||
"admin.users.account_request.invalid_action": "This account request action is not available for the current request state.",
|
||||
"admin.users.account_request.active_managed_externally": "Activation for this account is currently managed by the account request workflow.",
|
||||
"admin.users.account_request.status.pending_email_validation": "Waiting for email validation",
|
||||
"admin.users.account_request.status.pending_admin_review": "Waiting for administrator review",
|
||||
"admin.users.account_request.status.rejected": "Rejected",
|
||||
"admin.users.account_request.status.blocked": "Blocked",
|
||||
"admin.users.reset_2fa": "Reset 2FA",
|
||||
"admin.users.list_status_filter.menu_text": "Filter",
|
||||
"admin.users.list_status_filter.reset": "Reset",
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
)
|
||||
|
||||
func loadAccountRequestData(ctx *context.Context, user *user_model.User) bool {
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, user)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["AccountRequestStatus"] = status
|
||||
|
||||
attempts, err := user_model.GetAccountRequestSignupAttempts(ctx, user)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestSignupAttempts", err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["AccountRequestSignupAttempts"] = attempts
|
||||
return true
|
||||
}
|
||||
|
||||
func accountRequestRedirect(ctx *context.Context, userID int64) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(userID, 10) + "/edit")
|
||||
}
|
||||
|
||||
func AccountRequestActivate(ctx *context.Context) {
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return
|
||||
}
|
||||
if status != user_model.AccountRequestStatusPendingAdminReview {
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.account_request.invalid_action"))
|
||||
accountRequestRedirect(ctx, u.ID)
|
||||
return
|
||||
}
|
||||
if err := user_model.ApproveAccountRequest(ctx, u, ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("ApproveAccountRequest", err)
|
||||
return
|
||||
}
|
||||
mailer.SendAccountRequestApprovedMail(u)
|
||||
log.Trace("Account request approved by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.account_request.approved"))
|
||||
accountRequestRedirect(ctx, u.ID)
|
||||
}
|
||||
|
||||
func AccountRequestReject(ctx *context.Context) {
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return
|
||||
}
|
||||
if status != user_model.AccountRequestStatusPendingAdminReview {
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.account_request.invalid_action"))
|
||||
accountRequestRedirect(ctx, u.ID)
|
||||
return
|
||||
}
|
||||
if err := user_model.RejectAccountRequest(ctx, u, ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("RejectAccountRequest", err)
|
||||
return
|
||||
}
|
||||
mailer.SendAccountRequestRejectedMail(u)
|
||||
log.Trace("Account request rejected by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.account_request.rejected"))
|
||||
accountRequestRedirect(ctx, u.ID)
|
||||
}
|
||||
|
||||
func AccountRequestUnblock(ctx *context.Context) {
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return
|
||||
}
|
||||
if status != user_model.AccountRequestStatusRejected && status != user_model.AccountRequestStatusBlocked {
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.account_request.invalid_action"))
|
||||
accountRequestRedirect(ctx, u.ID)
|
||||
return
|
||||
}
|
||||
if err := user_model.UnblockAccountRequest(ctx, u); err != nil {
|
||||
ctx.ServerError("UnblockAccountRequest", err)
|
||||
return
|
||||
}
|
||||
mailer.SendAccountRequestValidationMail(translation.NewLocale(u.Language), u)
|
||||
log.Trace("Account request unblocked by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.account_request.unblocked"))
|
||||
accountRequestRedirect(ctx, u.ID)
|
||||
}
|
||||
@@ -323,10 +323,13 @@ func editUserCommon(ctx *context.Context) {
|
||||
// EditUser show editing user page
|
||||
func EditUser(ctx *context.Context) {
|
||||
editUserCommon(ctx)
|
||||
prepareUserInfo(ctx)
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if !loadAccountRequestData(ctx, u) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserEdit)
|
||||
}
|
||||
@@ -428,12 +431,21 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
requestStatus, err := user_model.GetAccountRequestStatus(ctx, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return
|
||||
}
|
||||
wasActive := u.IsActive
|
||||
requestedActive := form.Active
|
||||
if requestStatus != "" {
|
||||
requestedActive = u.IsActive
|
||||
}
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.Some(form.FullName),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
IsActive: optional.Some(form.Active),
|
||||
IsActive: optional.Some(requestedActive),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromValue(form.Admin),
|
||||
AllowGitHook: optional.Some(form.AllowGitHook),
|
||||
AllowImportLocal: optional.Some(form.AllowImportLocal),
|
||||
@@ -454,7 +466,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
if !wasActive && form.Active && setting.Service.RegisterManualConfirm && !setting.Service.RegisterEmailConfirm {
|
||||
if !wasActive && requestedActive && requestStatus == "" && setting.Service.RegisterManualConfirm && !setting.Service.RegisterEmailConfirm {
|
||||
mailer.SendActivateAccountMail(translation.NewLocale(u.Language), u)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func renderPendingAccountRequestSignup(ctx *context.Context, form *forms.RegisterForm, email string, message template.HTML, showResend bool) {
|
||||
ctx.Data["PendingAccountRequestMessage"] = message
|
||||
ctx.Data["PendingAccountRequestEmail"] = email
|
||||
ctx.Data["ShowResendAccountRequestButton"] = showResend
|
||||
ctx.HTML(http.StatusOK, tplSignUp)
|
||||
}
|
||||
|
||||
func handleExistingAccountRequestOnSignup(ctx *context.Context, form *forms.RegisterForm) bool {
|
||||
existing, err := user_model.GetUserByAnyEmail(ctx, form.Email)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
return true
|
||||
}
|
||||
ctx.ServerError("GetUserByAnyEmail", err)
|
||||
return false
|
||||
}
|
||||
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, existing)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return false
|
||||
}
|
||||
|
||||
switch status {
|
||||
case user_model.AccountRequestStatusPendingEmailValidation:
|
||||
expired, err := user_model.IsAccountRequestExpired(ctx, existing)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsAccountRequestExpired", err)
|
||||
return false
|
||||
}
|
||||
if expired {
|
||||
if err := user_service.DeleteUser(ctx, existing, false); err != nil {
|
||||
ctx.ServerError("DeleteUser", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
attempts, blocked, err := user_model.IncrementAccountRequestSignupAttempts(ctx, existing)
|
||||
if err != nil {
|
||||
ctx.ServerError("IncrementAccountRequestSignupAttempts", err)
|
||||
return false
|
||||
}
|
||||
if blocked {
|
||||
renderPendingAccountRequestSignup(ctx, form, form.Email, ctx.Tr("auth.account_request_blocked", user_model.AccountRequestMaxAttempts), false)
|
||||
return false
|
||||
}
|
||||
_ = attempts
|
||||
renderPendingAccountRequestSignup(ctx, form, form.Email, ctx.Tr("auth.account_request_validation_pending_signup"), true)
|
||||
return false
|
||||
case user_model.AccountRequestStatusPendingAdminReview:
|
||||
renderPendingAccountRequestSignup(ctx, form, form.Email, ctx.Tr("auth.account_request_admin_review_pending"), false)
|
||||
return false
|
||||
case user_model.AccountRequestStatusRejected:
|
||||
renderPendingAccountRequestSignup(ctx, form, form.Email, ctx.Tr("auth.account_request_rejected_signup"), false)
|
||||
return false
|
||||
case user_model.AccountRequestStatusBlocked:
|
||||
renderPendingAccountRequestSignup(ctx, form, form.Email, ctx.Tr("auth.account_request_blocked", user_model.AccountRequestMaxAttempts), false)
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func handleAccountRequestValidation(ctx *context.Context, user *user_model.User) {
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, user)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch status {
|
||||
case user_model.AccountRequestStatusPendingEmailValidation:
|
||||
expired, err := user_model.IsAccountRequestExpired(ctx, user)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsAccountRequestExpired", err)
|
||||
return
|
||||
}
|
||||
if expired {
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.account_request_validation_expired"))
|
||||
return
|
||||
}
|
||||
if err := user_model.MarkAccountRequestPendingAdminReview(ctx, user); err != nil {
|
||||
ctx.ServerError("MarkAccountRequestPendingAdminReview", err)
|
||||
return
|
||||
}
|
||||
if err := mailer.SendNewAccountRequestMail(ctx, user); err != nil {
|
||||
log.Error("SendNewAccountRequestMail: %v", err)
|
||||
}
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.account_request_validated_waiting_admin"))
|
||||
case user_model.AccountRequestStatusPendingAdminReview:
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.account_request_admin_review_pending"))
|
||||
case user_model.AccountRequestStatusRejected:
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.account_request_rejected_signup"))
|
||||
case user_model.AccountRequestStatusBlocked:
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.account_request_blocked", user_model.AccountRequestMaxAttempts))
|
||||
default:
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||
}
|
||||
}
|
||||
|
||||
func ResendAccountRequestValidationPost(ctx *context.Context) {
|
||||
if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(ctx.FormString("email"))
|
||||
user, err := user_model.GetUserByAnyEmail(ctx, email)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("auth.account_request_resend_not_available"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/sign_up")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetUserByAnyEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, user)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAccountRequestStatus", err)
|
||||
return
|
||||
}
|
||||
if status != user_model.AccountRequestStatusPendingEmailValidation {
|
||||
ctx.Flash.Error(ctx.Tr("auth.account_request_resend_not_available"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/sign_up")
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.RefreshAccountRequestValidationWindow(ctx, user); err != nil {
|
||||
ctx.ServerError("RefreshAccountRequestValidationWindow", err)
|
||||
return
|
||||
}
|
||||
mailer.SendAccountRequestValidationMail(ctx.Locale, user)
|
||||
ctx.Data["PendingAccountRequestMessage"] = ctx.Tr("auth.account_request_validation_resent")
|
||||
ctx.Data["PendingAccountRequestEmail"] = email
|
||||
ctx.Data["ShowResendAccountRequestButton"] = true
|
||||
prepareSignUpPageData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplSignUp)
|
||||
}
|
||||
@@ -575,6 +575,10 @@ func SignUpPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !handleExistingAccountRequestOnSignup(ctx, form) {
|
||||
return
|
||||
}
|
||||
|
||||
if form.Password != form.Retype {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplSignUp, &form)
|
||||
@@ -730,10 +734,12 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, possibleLinkAcc
|
||||
}
|
||||
|
||||
if setting.Service.RegisterManualConfirm {
|
||||
if err := mailer.SendNewAccountRequestMail(ctx, u); err != nil {
|
||||
log.Error("SendNewAccountRequestMail: %v", err)
|
||||
if err := user_model.ResetAccountRequestToPendingEmailValidation(ctx, u); err != nil {
|
||||
ctx.ServerError("ResetAccountRequestToPendingEmailValidation", err)
|
||||
return false
|
||||
}
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.manual_activation_only"))
|
||||
mailer.SendAccountRequestValidationMail(ctx.Locale, u)
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.account_request_validation_sent"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -799,6 +805,11 @@ func Activate(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||
if user := user_model.VerifyAccountRequestValidationCode(ctx, code); user != nil {
|
||||
handleAccountRequestValidation(ctx, user)
|
||||
return
|
||||
}
|
||||
|
||||
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||
if user == nil { // if code is wrong
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||
@@ -844,6 +855,11 @@ func ActivatePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||
if user := user_model.VerifyAccountRequestValidationCode(ctx, code); user != nil {
|
||||
handleAccountRequestValidation(ctx, user)
|
||||
return
|
||||
}
|
||||
|
||||
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||
if user == nil { // if code is wrong
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||
|
||||
@@ -569,6 +569,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, openIDSignInEnabled)
|
||||
m.Get("/sign_up", auth.SignUp)
|
||||
m.Post("/sign_up", web.Bind(forms.RegisterForm{}), auth.SignUpPost)
|
||||
m.Post("/sign_up/resend", auth.ResendAccountRequestValidationPost)
|
||||
m.Get("/link_account", auth.LinkAccount)
|
||||
m.Post("/link_account_signin", web.Bind(forms.SignInForm{}), auth.LinkAccountPostSignIn)
|
||||
m.Post("/link_account_signup", web.Bind(forms.RegisterForm{}), auth.LinkAccountPostRegister)
|
||||
@@ -784,6 +785,9 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Combo("/new").Get(admin.NewUser).Post(web.Bind(forms.AdminCreateUserForm{}), admin.NewUserPost)
|
||||
m.Get("/{userid}", admin.ViewUser)
|
||||
m.Combo("/{userid}/edit").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost)
|
||||
m.Post("/{userid}/request/activate", admin.AccountRequestActivate)
|
||||
m.Post("/{userid}/request/reject", admin.AccountRequestReject)
|
||||
m.Post("/{userid}/request/unblock", admin.AccountRequestUnblock)
|
||||
m.Post("/{userid}/delete", admin.DeleteUser)
|
||||
m.Post("/{userid}/avatar", web.Bind(forms.AvatarForm{}), admin.AvatarPost)
|
||||
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
|
||||
|
||||
@@ -34,6 +34,16 @@ func registerDeleteInactiveUsers() {
|
||||
})
|
||||
}
|
||||
|
||||
func registerDeleteExpiredAccountRequests() {
|
||||
RegisterTaskFatal("delete_expired_account_requests", &BaseConfig{
|
||||
Enabled: true,
|
||||
RunAtStart: false,
|
||||
Schedule: "@every 1h",
|
||||
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
|
||||
return user_service.DeleteExpiredAccountRequests(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func registerDeleteRepositoryArchives() {
|
||||
RegisterTaskFatal("delete_repo_archives", &BaseConfig{
|
||||
Enabled: false,
|
||||
@@ -226,6 +236,7 @@ func registerRebuildIssueIndexer() {
|
||||
|
||||
func initExtendedTasks() {
|
||||
registerDeleteInactiveUsers()
|
||||
registerDeleteExpiredAccountRequests()
|
||||
registerDeleteRepositoryArchives()
|
||||
registerGarbageCollectRepositories()
|
||||
registerRewriteAllPublicKeys()
|
||||
|
||||
@@ -20,11 +20,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
mailAuthActivate templates.TplName = "user/auth/activate"
|
||||
mailAuthActivateEmail templates.TplName = "user/auth/activate_email"
|
||||
mailAuthNewAccountRequest templates.TplName = "user/auth/new_account_request"
|
||||
mailAuthResetPassword templates.TplName = "user/auth/reset_passwd"
|
||||
mailAuthRegisterNotify templates.TplName = "user/auth/register_notify"
|
||||
mailAuthActivate templates.TplName = "user/auth/activate"
|
||||
mailAuthActivateEmail templates.TplName = "user/auth/activate_email"
|
||||
mailAuthAccountRequestApproved templates.TplName = "user/auth/account_request_approved"
|
||||
mailAuthAccountRequestRejected templates.TplName = "user/auth/account_request_rejected"
|
||||
mailAuthAccountRequestValidate templates.TplName = "user/auth/account_request_validate"
|
||||
mailAuthNewAccountRequest templates.TplName = "user/auth/new_account_request"
|
||||
mailAuthResetPassword templates.TplName = "user/auth/reset_passwd"
|
||||
mailAuthRegisterNotify templates.TplName = "user/auth/register_notify"
|
||||
)
|
||||
|
||||
// sendUserMail sends a mail to the user
|
||||
@@ -52,20 +55,65 @@ func sendUserMail(language string, u *user_model.User, tpl templates.TplName, co
|
||||
SendAsync(msg)
|
||||
}
|
||||
|
||||
func sendSimpleUserMail(language string, u *user_model.User, tpl templates.TplName, subject, info string, data map[string]any) {
|
||||
locale := translation.NewLocale(language)
|
||||
if data == nil {
|
||||
data = map[string]any{}
|
||||
}
|
||||
data["locale"] = locale
|
||||
data["DisplayName"] = u.DisplayName()
|
||||
data["Language"] = locale.Language()
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
msg := sender_service.NewMessage(u.EmailTo(), subject, content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
|
||||
SendAsync(msg)
|
||||
}
|
||||
|
||||
// SendActivateAccountMail sends an activation mail to the user (new user registration)
|
||||
func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
}
|
||||
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}
|
||||
sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account")
|
||||
}
|
||||
|
||||
func SendAccountRequestValidationMail(locale translation.Locale, u *user_model.User) {
|
||||
if setting.MailService == nil {
|
||||
return
|
||||
}
|
||||
validationURL := fmt.Sprintf("%suser/activate?code=%s", setting.AppURL, user_model.GenerateAccountRequestValidationCode(u))
|
||||
sendSimpleUserMail(locale.Language(), u, mailAuthAccountRequestValidate, locale.TrString("mail.account_request.validate"), "account request validation", map[string]any{
|
||||
"ValidationURL": validationURL,
|
||||
"AccountRequestCodeLives": timeutil.MinutesToFriendly(user_model.AccountRequestValidationHours*60, locale),
|
||||
})
|
||||
}
|
||||
|
||||
func SendAccountRequestApprovedMail(u *user_model.User) {
|
||||
if setting.MailService == nil {
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
sendSimpleUserMail(u.Language, u, mailAuthAccountRequestApproved, locale.TrString("mail.account_request.approved", setting.AppName), "account request approved", nil)
|
||||
}
|
||||
|
||||
func SendAccountRequestRejectedMail(u *user_model.User) {
|
||||
if setting.MailService == nil {
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
sendSimpleUserMail(u.Language, u, mailAuthAccountRequestRejected, locale.TrString("mail.account_request.rejected", setting.AppName), "account request rejected", nil)
|
||||
}
|
||||
|
||||
// SendResetPasswordMail sends a password reset mail to the user
|
||||
func SendResetPasswordMail(u *user_model.User) {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
@@ -76,7 +124,6 @@ func SendResetPasswordMail(u *user_model.User) {
|
||||
// SendActivateEmailMail sends confirmation email to confirm new email address
|
||||
func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
@@ -91,7 +138,6 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
@@ -99,18 +145,15 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
|
||||
msg := sender_service.NewMessage(email, locale.TrString("mail.activate_email"), content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
|
||||
|
||||
SendAsync(msg)
|
||||
}
|
||||
|
||||
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
|
||||
func SendRegisterNotifyMail(u *user_model.User) {
|
||||
if setting.MailService == nil || !u.IsActive {
|
||||
// No mail service configured OR user is inactive
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
|
||||
data := map[string]any{
|
||||
"locale": locale,
|
||||
"DisplayName": u.DisplayName(),
|
||||
@@ -119,7 +162,6 @@ func SendRegisterNotifyMail(u *user_model.User) {
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
@@ -127,7 +169,6 @@ func SendRegisterNotifyMail(u *user_model.User) {
|
||||
|
||||
msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
|
||||
|
||||
SendAsync(msg)
|
||||
}
|
||||
|
||||
@@ -156,7 +197,6 @@ func SendNewAccountRequestMail(ctx context.Context, requester *user_model.User)
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
langMap[admin.Language] = append(langMap[admin.Language], admin)
|
||||
}
|
||||
|
||||
@@ -165,7 +205,6 @@ func SendNewAccountRequestMail(ctx context.Context, requester *user_model.User)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -182,7 +221,6 @@ func sendNewAccountRequestMailPerLang(lang string, emailTos []*user_model.User,
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthNewAccountRequest), data); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,6 +230,5 @@ func sendNewAccountRequestMailPerLang(lang string, emailTos []*user_model.User,
|
||||
msg.Info = fmt.Sprintf("UID: %d, new account request notification", requester.ID)
|
||||
SendAsync(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+22
-3
@@ -294,10 +294,25 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: should only update authorized_keys file once after all deletions.
|
||||
for _, u := range inactiveUsers {
|
||||
status, err := user_model.GetAccountRequestStatus(ctx, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch status {
|
||||
case user_model.AccountRequestStatusPendingAdminReview, user_model.AccountRequestStatusRejected, user_model.AccountRequestStatusBlocked:
|
||||
continue
|
||||
case user_model.AccountRequestStatusPendingEmailValidation:
|
||||
expired, err := user_model.IsAccountRequestExpired(ctx, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !expired {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err = DeleteUser(ctx, u, false); err != nil {
|
||||
// Ignore inactive users that were ever active but then were set inactive by admin
|
||||
if repo_model.IsErrUserOwnRepos(err) || organization.IsErrUserHasOrgs(err) || packages_model.IsErrUserOwnPackages(err) {
|
||||
log.Warn("Inactive user %q has repositories, organizations or packages, skipping deletion: %v", u.Name, err)
|
||||
continue
|
||||
@@ -310,5 +325,9 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil // TODO: there could be still inactive users left, and the number would increase gradually
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteExpiredAccountRequests(ctx context.Context) error {
|
||||
return user_model.DeleteExpiredPendingAccountRequests(ctx)
|
||||
}
|
||||
|
||||
@@ -103,10 +103,18 @@
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{{if .AccountRequestStatus}}
|
||||
<div class="ui info message">
|
||||
<p><strong>{{ctx.Locale.Tr "admin.users.account_request.status"}}</strong>: {{ctx.Locale.Tr (printf "admin.users.account_request.status.%s" .AccountRequestStatus)}}</p>
|
||||
{{if .AccountRequestSignupAttempts}}<p>{{ctx.Locale.Tr "admin.users.account_request.attempts" .AccountRequestSignupAttempts}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label><strong>{{ctx.Locale.Tr "admin.users.is_activated"}}</strong></label>
|
||||
<input name="active" type="checkbox" {{if .User.IsActive}}checked{{end}}>
|
||||
<input name="active" type="checkbox" {{if .User.IsActive}}checked{{end}} {{if .AccountRequestStatus}}disabled{{end}}>
|
||||
{{if .AccountRequestStatus}}<p class="help">{{ctx.Locale.Tr "admin.users.account_request.active_managed_externally"}}</p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
@@ -169,6 +177,30 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{if .AccountRequestStatus}}
|
||||
<div class="ui attached segment">
|
||||
{{if eq .AccountRequestStatus "pending_admin_review"}}
|
||||
<div class="field tw-flex tw-flex-wrap tw-gap-2">
|
||||
<form method="post" action="./request/activate">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "admin.users.account_request.activate"}}</button>
|
||||
</form>
|
||||
<form method="post" action="./request/reject">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui red button">{{ctx.Locale.Tr "admin.users.account_request.reject"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{else if or (eq .AccountRequestStatus "blocked") (eq .AccountRequestStatus "rejected")}}
|
||||
<div class="field">
|
||||
<form method="post" action="./request/unblock">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "admin.users.account_request.unblock"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.avatar"}}
|
||||
</h4>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
|
||||
<title>{{.locale.Tr "mail.account_request.approved.title" AppName}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>{{.locale.Tr "mail.account_request.approved.text_1" (.DisplayName|DotEscape) AppName}}</p><br>
|
||||
<p>{{.locale.Tr "mail.account_request.approved.text_2"}}</p><br>
|
||||
<p>© <a href="{{AppUrl}}">{{AppName}}</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
|
||||
<title>{{.locale.Tr "mail.account_request.rejected.title" AppName}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>{{.locale.Tr "mail.account_request.rejected.text_1" (.DisplayName|DotEscape) AppName}}</p><br>
|
||||
<p>{{.locale.Tr "mail.account_request.rejected.text_2"}}</p><br>
|
||||
<p>© <a href="{{AppUrl}}">{{AppName}}</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
|
||||
<title>{{.locale.Tr "mail.account_request.validate.title" (.DisplayName|DotEscape)}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>{{.locale.Tr "mail.account_request.validate.text_1" (.DisplayName|DotEscape) AppName}}</p><br>
|
||||
<p>{{.locale.Tr "mail.account_request.validate.text_2" .AccountRequestCodeLives}}</p><p><a href="{{.ValidationURL}}">{{.ValidationURL}}</a></p><br>
|
||||
<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
|
||||
|
||||
<p>© <a href="{{AppUrl}}">{{AppName}}</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,10 +10,22 @@
|
||||
{{if .IsFirstTimeRegistration}}
|
||||
<p>{{ctx.Locale.Tr "auth.sign_up_tip"}}</p>
|
||||
{{end}}
|
||||
<form class="ui form" action="{{.SignUpLink}}" method="post">
|
||||
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
|
||||
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
|
||||
{{template "base/alert" .}}
|
||||
{{if .PendingAccountRequestMessage}}
|
||||
<div class="ui info message">
|
||||
<p>{{.PendingAccountRequestMessage}}</p>
|
||||
{{if .ShowResendAccountRequestButton}}
|
||||
<form class="ui form" action="{{AppSubUrl}}/user/sign_up/resend" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="email" value="{{.PendingAccountRequestEmail}}">
|
||||
<button class="ui button" type="submit">{{ctx.Locale.Tr "auth.account_request_resend"}}</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<form class="ui form" action="{{.SignUpLink}}" method="post">
|
||||
{{if .DisableRegistration}}
|
||||
<p>{{ctx.Locale.Tr "auth.disable_register_prompt"}}</p>
|
||||
{{else}}
|
||||
|
||||
Reference in New Issue
Block a user