Skip to content

Commit aa45777

Browse files
authored
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".
1 parent a0b9767 commit aa45777

File tree

5 files changed

+91
-1
lines changed

5 files changed

+91
-1
lines changed

models/user/user.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,11 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
640640
u.IsRestricted = setting.Service.DefaultUserIsRestricted
641641
u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm)
642642

643+
// Ensure consistency of the dates.
644+
if u.UpdatedUnix < u.CreatedUnix {
645+
u.UpdatedUnix = u.CreatedUnix
646+
}
647+
643648
// overwrite defaults if set
644649
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
645650
overwrite := overwriteDefault[0]
@@ -717,7 +722,15 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
717722
return err
718723
}
719724

720-
if err = db.Insert(ctx, u); err != nil {
725+
if u.CreatedUnix == 0 {
726+
// Caller expects auto-time for creation & update timestamps.
727+
err = db.Insert(ctx, u)
728+
} else {
729+
// Caller sets the timestamps themselves. They are responsible for ensuring
730+
// both `CreatedUnix` and `UpdatedUnix` are set appropriately.
731+
_, err = db.GetEngine(ctx).NoAutoTime().Insert(u)
732+
}
733+
if err != nil {
721734
return err
722735
}
723736

models/user/user_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
package user_test
55

66
import (
7+
"context"
78
"math/rand"
89
"strings"
910
"testing"
11+
"time"
1012

1113
"code.gitea.io/gitea/models/auth"
1214
"code.gitea.io/gitea/models/db"
1315
"code.gitea.io/gitea/models/unittest"
1416
user_model "code.gitea.io/gitea/models/user"
1517
"code.gitea.io/gitea/modules/setting"
1618
"code.gitea.io/gitea/modules/structs"
19+
"code.gitea.io/gitea/modules/timeutil"
1720
"code.gitea.io/gitea/modules/util"
1821

1922
"github.com/stretchr/testify/assert"
@@ -252,6 +255,58 @@ func TestCreateUserEmailAlreadyUsed(t *testing.T) {
252255
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
253256
}
254257

258+
func TestCreateUserCustomTimestamps(t *testing.T) {
259+
assert.NoError(t, unittest.PrepareTestDatabase())
260+
261+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
262+
263+
// Add new user with a custom creation timestamp.
264+
var creationTimestamp timeutil.TimeStamp = 12345
265+
user.Name = "testuser"
266+
user.LowerName = strings.ToLower(user.Name)
267+
user.ID = 0
268+
user.Email = "[email protected]"
269+
user.CreatedUnix = creationTimestamp
270+
err := user_model.CreateUser(user)
271+
assert.NoError(t, err)
272+
273+
fetched, err := user_model.GetUserByID(context.Background(), user.ID)
274+
assert.NoError(t, err)
275+
assert.Equal(t, creationTimestamp, fetched.CreatedUnix)
276+
assert.Equal(t, creationTimestamp, fetched.UpdatedUnix)
277+
}
278+
279+
func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
280+
assert.NoError(t, unittest.PrepareTestDatabase())
281+
282+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
283+
284+
// There is no way to use a mocked time for the XORM auto-time functionality,
285+
// so use the real clock to approximate the expected timestamp.
286+
timestampStart := time.Now().Unix()
287+
288+
// Add new user without a custom creation timestamp.
289+
user.Name = "Testuser"
290+
user.LowerName = strings.ToLower(user.Name)
291+
user.ID = 0
292+
user.Email = "[email protected]"
293+
user.CreatedUnix = 0
294+
user.UpdatedUnix = 0
295+
err := user_model.CreateUser(user)
296+
assert.NoError(t, err)
297+
298+
timestampEnd := time.Now().Unix()
299+
300+
fetched, err := user_model.GetUserByID(context.Background(), user.ID)
301+
assert.NoError(t, err)
302+
303+
assert.LessOrEqual(t, timestampStart, fetched.CreatedUnix)
304+
assert.LessOrEqual(t, fetched.CreatedUnix, timestampEnd)
305+
306+
assert.LessOrEqual(t, timestampStart, fetched.UpdatedUnix)
307+
assert.LessOrEqual(t, fetched.UpdatedUnix, timestampEnd)
308+
}
309+
255310
func TestGetUserIDsByNames(t *testing.T) {
256311
assert.NoError(t, unittest.PrepareTestDatabase())
257312

modules/structs/admin_user.go

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package structs
66

7+
import "time"
8+
79
// CreateUserOption create user options
810
type CreateUserOption struct {
911
SourceID int64 `json:"source_id"`
@@ -20,6 +22,11 @@ type CreateUserOption struct {
2022
SendNotify bool `json:"send_notify"`
2123
Restricted *bool `json:"restricted"`
2224
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
25+
26+
// For explicitly setting the user creation timestamp. Useful when users are
27+
// migrated from other systems. When omitted, the user's creation timestamp
28+
// will be set to "now".
29+
Created *time.Time `json:"created_at"`
2330
}
2431

2532
// EditUserOption edit user options

routers/api/v1/admin/user.go

+9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"code.gitea.io/gitea/modules/password"
2121
"code.gitea.io/gitea/modules/setting"
2222
api "code.gitea.io/gitea/modules/structs"
23+
"code.gitea.io/gitea/modules/timeutil"
2324
"code.gitea.io/gitea/modules/util"
2425
"code.gitea.io/gitea/modules/web"
2526
"code.gitea.io/gitea/routers/api/v1/user"
@@ -120,6 +121,14 @@ func CreateUser(ctx *context.APIContext) {
120121
overwriteDefault.Visibility = &visibility
121122
}
122123

124+
// Update the user creation timestamp. This can only be done after the user
125+
// record has been inserted into the database; the insert intself will always
126+
// set the creation timestamp to "now".
127+
if form.Created != nil {
128+
u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
129+
u.UpdatedUnix = u.CreatedUnix
130+
}
131+
123132
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
124133
if user_model.IsErrUserAlreadyExist(err) ||
125134
user_model.IsErrEmailAlreadyUsed(err) ||

templates/swagger/v1_json.tmpl

+6
Original file line numberDiff line numberDiff line change
@@ -15809,6 +15809,12 @@
1580915809
"password"
1581015810
],
1581115811
"properties": {
15812+
"created_at": {
15813+
"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\".",
15814+
"type": "string",
15815+
"format": "date-time",
15816+
"x-go-name": "Created"
15817+
},
1581215818
"email": {
1581315819
"type": "string",
1581415820
"format": "email",

0 commit comments

Comments
 (0)