2021-11-11 15:03:30 +08:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2021-11-11 15:03:30 +08:00
package user
import (
"context"
"fmt"
2025-01-24 04:16:56 +00:00
"slices"
"strconv"
2021-11-11 15:03:30 +08:00
"strings"
2025-01-24 04:16:56 +00:00
"time"
2021-11-11 15:03:30 +08:00
"code.gitea.io/gitea/models/db"
2025-01-24 04:16:56 +00:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
2022-10-18 06:50:37 +01:00
"code.gitea.io/gitea/modules/util"
2025-01-24 04:16:56 +00:00
"xorm.io/builder"
2021-11-11 15:03:30 +08:00
)
// 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 )
}
2022-10-18 06:50:37 +01:00
func ( err ErrUserRedirectNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2025-01-24 04:16:56 +00:00
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 )
}
2021-11-11 15:03:30 +08:00
// Redirect represents that a user name should be redirected to another
type Redirect struct {
2025-01-24 04:16:56 +00:00
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" `
2021-11-11 15:03:30 +08:00
}
// TableName provides the real table name
func ( Redirect ) TableName ( ) string {
return "user_redirect"
}
func init ( ) {
db . RegisterModel ( new ( Redirect ) )
}
2025-01-24 04:16:56 +00:00
// GetUserRedirect returns the redirect for a given username, this is a
// case-insenstive operation.
func GetUserRedirect ( ctx context . Context , userName string ) ( * Redirect , error ) {
2021-11-11 15:03:30 +08:00
userName = strings . ToLower ( userName )
redirect := & Redirect { LowerName : userName }
2023-09-25 15:17:37 +02:00
if has , err := db . GetEngine ( ctx ) . Get ( redirect ) ; err != nil {
2025-01-24 04:16:56 +00:00
return nil , err
2021-11-11 15:03:30 +08:00
} else if ! has {
2025-01-24 04:16:56 +00:00
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
2021-11-11 15:03:30 +08:00
}
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 )
2023-07-03 18:05:59 +08:00
if err := DeleteUserRedirect ( ctx , oldUserName ) ; err != nil {
return err
}
2021-11-11 15:03:30 +08:00
if err := DeleteUserRedirect ( ctx , newUserName ) ; err != nil {
return err
}
return db . Insert ( ctx , & Redirect {
LowerName : oldUserName ,
RedirectUserID : ID ,
} )
}
2025-01-24 04:16:56 +00:00
// 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
}
2021-11-11 15:03:30 +08:00
// 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
}
2025-01-24 04:16:56 +00:00
// 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
}