// Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package user import ( "context" "fmt" "slices" "strconv" "strings" "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) // ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error. type ErrUserRedirectNotExist struct { Name string } // IsErrUserRedirectNotExist check if an error is an ErrUserRedirectNotExist. func IsErrUserRedirectNotExist(err error) bool { _, ok := err.(ErrUserRedirectNotExist) return ok } func (err ErrUserRedirectNotExist) Error() string { return fmt.Sprintf("user redirect does not exist [name: %s]", err.Name) } func (err ErrUserRedirectNotExist) Unwrap() error { return util.ErrNotExist } type ErrCooldownPeriod struct { ExpireTime time.Time } func IsErrCooldownPeriod(err error) bool { _, ok := err.(ErrCooldownPeriod) return ok } func (err ErrCooldownPeriod) Error() string { return fmt.Sprintf("cooldown period for claiming this username has not yet expired: the cooldown period ends at %s", err.ExpireTime) } // Redirect represents that a user name should be redirected to another type Redirect struct { ID int64 `xorm:"pk autoincr"` LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` RedirectUserID int64 // userID to redirect to CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL DEFAULT 0"` } // TableName provides the real table name func (Redirect) TableName() string { return "user_redirect" } func init() { db.RegisterModel(new(Redirect)) } // GetUserRedirect returns the redirect for a given username, this is a // case-insenstive operation. func GetUserRedirect(ctx context.Context, userName string) (*Redirect, error) { userName = strings.ToLower(userName) redirect := &Redirect{LowerName: userName} if has, err := db.GetEngine(ctx).Get(redirect); err != nil { return nil, err } else if !has { return nil, ErrUserRedirectNotExist{Name: userName} } return redirect, nil } // LookupUserRedirect look up userID if a user has a redirect name func LookupUserRedirect(ctx context.Context, userName string) (int64, error) { redirect, err := GetUserRedirect(ctx, userName) if err != nil { return 0, err } return redirect.RedirectUserID, nil } // NewUserRedirect create a new user redirect func NewUserRedirect(ctx context.Context, ID int64, oldUserName, newUserName string) error { oldUserName = strings.ToLower(oldUserName) newUserName = strings.ToLower(newUserName) if err := DeleteUserRedirect(ctx, oldUserName); err != nil { return err } if err := DeleteUserRedirect(ctx, newUserName); err != nil { return err } return db.Insert(ctx, &Redirect{ LowerName: oldUserName, RedirectUserID: ID, }) } // LimitUserRedirects deletes the oldest entries in user_redirect of the user, // such that the amount of user_redirects is at most `n` amount of entries. func LimitUserRedirects(ctx context.Context, userID, n int64) error { // NOTE: It's not possible to combine these two queries into one due to a limitation of MySQL. keepIDs := make([]int64, n) if err := db.GetEngine(ctx).SQL("SELECT id FROM user_redirect WHERE redirect_user_id = ? ORDER BY created_unix DESC LIMIT "+strconv.FormatInt(n, 10), userID).Find(&keepIDs); err != nil { return err } _, err := db.GetEngine(ctx).Exec(builder.Delete(builder.And(builder.Eq{"redirect_user_id": userID}, builder.NotIn("id", keepIDs))).From("user_redirect")) return err } // DeleteUserRedirect delete any redirect from the specified user name to // anything else func DeleteUserRedirect(ctx context.Context, userName string) error { userName = strings.ToLower(userName) _, err := db.GetEngine(ctx).Delete(&Redirect{LowerName: userName}) return err } // CanClaimUsername returns if its possible to claim the given username, // it checks if the cooldown period for claiming an existing username is over. // If there's a cooldown period, the second argument returns the time when // that cooldown period is over. // In the scenario of renaming, the doerID can be specified to allow the original // user of the username to reclaim it within the cooldown period. func CanClaimUsername(ctx context.Context, username string, doerID int64) (bool, time.Time, error) { // Only check for a cooldown period if UsernameCooldownPeriod is a positive number. if setting.Service.UsernameCooldownPeriod <= 0 { return true, time.Time{}, nil } userRedirect, err := GetUserRedirect(ctx, username) if err != nil { if IsErrUserRedirectNotExist(err) { return true, time.Time{}, nil } return false, time.Time{}, err } // Allow reclaiming of user's own username. if userRedirect.RedirectUserID == doerID { return true, time.Time{}, nil } // We do not know if the redirect user id was for an organization, so // unconditionally execute the following query to retrieve all users that // are part of the "Owner" team. If the redirect user ID is not an organization // the returned list would be empty. ownerTeamUIDs := []int64{} if err := db.GetEngine(ctx).SQL("SELECT uid FROM team_user INNER JOIN team ON team_user.`team_id` = team.`id` WHERE team.`org_id` = ? AND team.`name` = 'Owners'", userRedirect.RedirectUserID).Find(&ownerTeamUIDs); err != nil { return false, time.Time{}, err } if slices.Contains(ownerTeamUIDs, doerID) { return true, time.Time{}, nil } // Multiply the value of UsernameCooldownPeriod by the amount of seconds in a day. expireTime := userRedirect.CreatedUnix.Add(86400 * setting.Service.UsernameCooldownPeriod).AsLocalTime() return time.Until(expireTime) <= 0, expireTime, nil }