Allow custom "created" timestamps in user creation API (#22549)

Allow back-dating user creation via the `adminCreateUser` API operation.
`CreateUserOption` now has an optional field `created_at`, which can
contain a datetime-formatted string. If this field is present, the
user's `created_unix` database field will be updated to its value.

This is important for Blender's migration of users from Phabricator to
Gitea. There are many users, and the creation timestamp of their account
can give us some indication as to how long someone's been part of the
community.

The back-dating is done in a separate query that just updates the user's
`created_unix` field. This was the easiest and cleanest way I could
find, as in the initial `INSERT` query the field always is set to "now".
This commit is contained in:
Sybren 2023-02-16 17:32:01 +01:00 committed by GitHub
parent a0b9767df8
commit aa45777c92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 91 additions and 1 deletions

View file

@ -640,6 +640,11 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
u.IsRestricted = setting.Service.DefaultUserIsRestricted u.IsRestricted = setting.Service.DefaultUserIsRestricted
u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm) u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm)
// Ensure consistency of the dates.
if u.UpdatedUnix < u.CreatedUnix {
u.UpdatedUnix = u.CreatedUnix
}
// overwrite defaults if set // overwrite defaults if set
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
overwrite := overwriteDefault[0] overwrite := overwriteDefault[0]
@ -717,7 +722,15 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
return err return err
} }
if err = db.Insert(ctx, u); err != nil { if u.CreatedUnix == 0 {
// Caller expects auto-time for creation & update timestamps.
err = db.Insert(ctx, u)
} else {
// Caller sets the timestamps themselves. They are responsible for ensuring
// both `CreatedUnix` and `UpdatedUnix` are set appropriately.
_, err = db.GetEngine(ctx).NoAutoTime().Insert(u)
}
if err != nil {
return err return err
} }

View file

@ -4,9 +4,11 @@
package user_test package user_test
import ( import (
"context"
"math/rand" "math/rand"
"strings" "strings"
"testing" "testing"
"time"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -14,6 +16,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -252,6 +255,58 @@ func TestCreateUserEmailAlreadyUsed(t *testing.T) {
assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
} }
func TestCreateUserCustomTimestamps(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Add new user with a custom creation timestamp.
var creationTimestamp timeutil.TimeStamp = 12345
user.Name = "testuser"
user.LowerName = strings.ToLower(user.Name)
user.ID = 0
user.Email = "unique@example.com"
user.CreatedUnix = creationTimestamp
err := user_model.CreateUser(user)
assert.NoError(t, err)
fetched, err := user_model.GetUserByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.Equal(t, creationTimestamp, fetched.CreatedUnix)
assert.Equal(t, creationTimestamp, fetched.UpdatedUnix)
}
func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// There is no way to use a mocked time for the XORM auto-time functionality,
// so use the real clock to approximate the expected timestamp.
timestampStart := time.Now().Unix()
// Add new user without a custom creation timestamp.
user.Name = "Testuser"
user.LowerName = strings.ToLower(user.Name)
user.ID = 0
user.Email = "unique@example.com"
user.CreatedUnix = 0
user.UpdatedUnix = 0
err := user_model.CreateUser(user)
assert.NoError(t, err)
timestampEnd := time.Now().Unix()
fetched, err := user_model.GetUserByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.LessOrEqual(t, timestampStart, fetched.CreatedUnix)
assert.LessOrEqual(t, fetched.CreatedUnix, timestampEnd)
assert.LessOrEqual(t, timestampStart, fetched.UpdatedUnix)
assert.LessOrEqual(t, fetched.UpdatedUnix, timestampEnd)
}
func TestGetUserIDsByNames(t *testing.T) { func TestGetUserIDsByNames(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View file

@ -4,6 +4,8 @@
package structs package structs
import "time"
// CreateUserOption create user options // CreateUserOption create user options
type CreateUserOption struct { type CreateUserOption struct {
SourceID int64 `json:"source_id"` SourceID int64 `json:"source_id"`
@ -20,6 +22,11 @@ type CreateUserOption struct {
SendNotify bool `json:"send_notify"` SendNotify bool `json:"send_notify"`
Restricted *bool `json:"restricted"` Restricted *bool `json:"restricted"`
Visibility string `json:"visibility" binding:"In(,public,limited,private)"` Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
// For explicitly setting the user creation timestamp. Useful when users are
// migrated from other systems. When omitted, the user's creation timestamp
// will be set to "now".
Created *time.Time `json:"created_at"`
} }
// EditUserOption edit user options // EditUserOption edit user options

View file

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
@ -120,6 +121,14 @@ func CreateUser(ctx *context.APIContext) {
overwriteDefault.Visibility = &visibility overwriteDefault.Visibility = &visibility
} }
// Update the user creation timestamp. This can only be done after the user
// record has been inserted into the database; the insert intself will always
// set the creation timestamp to "now".
if form.Created != nil {
u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
u.UpdatedUnix = u.CreatedUnix
}
if err := user_model.CreateUser(u, overwriteDefault); err != nil { if err := user_model.CreateUser(u, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) || if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) || user_model.IsErrEmailAlreadyUsed(err) ||

View file

@ -15809,6 +15809,12 @@
"password" "password"
], ],
"properties": { "properties": {
"created_at": {
"description": "For explicitly setting the user creation timestamp. Useful when users are\nmigrated from other systems. When omitted, the user's creation timestamp\nwill be set to \"now\".",
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"email": { "email": {
"type": "string", "type": "string",
"format": "email", "format": "email",