mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-19 10:24:51 +02:00
refactor(api): convert bot accounts through the admin user edit endpoint (#39355)
Follow-up to https://github.com/go-gitea/gitea/pull/38966. Replaces the unreleased `POST /admin/users/{username}/convert-type` endpoint with a `type` field on `PATCH /admin/users/{username}`.
This commit is contained in:
@@ -76,13 +76,6 @@ type EditUserOption struct {
|
||||
Restricted *bool `json:"restricted"`
|
||||
// User visibility level: public, limited, or private
|
||||
Visibility VisibilityString `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
}
|
||||
|
||||
// ConvertUserTypeOption options when converting a user between individual and bot
|
||||
type ConvertUserTypeOption struct {
|
||||
// The target user type
|
||||
//
|
||||
// required: true
|
||||
// enum: ["User","Bot"]
|
||||
UserType UserTypeString `json:"user_type" binding:"Required;In(User,Bot)"`
|
||||
// The user type
|
||||
Type UserTypeString `json:"type" binding:"In(User,Organization,Bot)"`
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type User struct {
|
||||
ID int64 `json:"id"`
|
||||
// login of the user, same as `username`
|
||||
UserName string `json:"login"`
|
||||
// the account type
|
||||
// the user type
|
||||
Type UserTypeString `json:"type"`
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
LoginName string `json:"login_name"`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package structs
|
||||
|
||||
// UserTypeString defines the account type as rendered in API responses, webhook payloads and
|
||||
// UserTypeString defines the user type as rendered in API responses, webhook payloads and
|
||||
// the resulting GitHub Actions event context, where workflows read it as `github.event.sender.type`.
|
||||
// The values are capitalized to stay compatible with GitHub, unlike VisibilityString and the other
|
||||
// lowercase API enums. The DB representation is user.UserType (int).
|
||||
|
||||
@@ -3083,7 +3083,6 @@
|
||||
"admin.users.impersonate_stop": "Stop impersonating",
|
||||
"admin.users.impersonating_notice": "You are impersonating <strong>%s</strong>. Actions you take are performed as this user.",
|
||||
"admin.users.user_type": "User Type",
|
||||
"admin.users.convert_type.not_convertible": "This user type cannot be converted. Only user and bot accounts support type conversion.",
|
||||
"admin.users.convert_type.admin_not_allowed": "Administrators cannot be converted into bot accounts. Remove the administrator permission first.",
|
||||
"admin.users.bot_token_desc": "Bot accounts cannot sign in, so their access tokens are managed here by administrators.",
|
||||
"admin.users.bot_token_only": "Access tokens can only be generated for bot accounts here.",
|
||||
|
||||
@@ -190,6 +190,16 @@ func EditUser(ctx *context.APIContext) {
|
||||
|
||||
form := web.GetForm[*api.EditUserOption](ctx)
|
||||
|
||||
var userType optional.Option[user_model.UserType]
|
||||
if form.Type != "" && form.Type != convert.UserTypeToString(ctx.ContextUser.Type) {
|
||||
newType, err := convert.UserTypeFromString(form.Type)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
userType = optional.Some(newType)
|
||||
}
|
||||
|
||||
authOpts := &user_service.UpdateAuthOptions{
|
||||
LoginSource: optional.FromNonDefault(form.SourceID),
|
||||
LoginName: optional.FromPtr(form.LoginName),
|
||||
@@ -197,6 +207,10 @@ func EditUser(ctx *context.APIContext) {
|
||||
MustChangePassword: optional.FromPtr(form.MustChangePassword),
|
||||
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
|
||||
}
|
||||
if userType.Value() == user_model.UserTypeBot && (authOpts.Password.Has() || authOpts.LoginSource.Value() != 0 || authOpts.LoginName.Value() != "") {
|
||||
ctx.APIError(http.StatusBadRequest, "a bot account cannot have a password or authentication source")
|
||||
return
|
||||
}
|
||||
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
@@ -231,6 +245,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
||||
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
|
||||
IsRestricted: optional.FromPtr(form.Restricted),
|
||||
UserType: userType,
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
|
||||
@@ -552,46 +567,3 @@ func RenameUser(ctx *context.APIContext) {
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ConvertUserType converts an account between the user and bot types
|
||||
func ConvertUserType(ctx *context.APIContext) {
|
||||
// swagger:operation POST /admin/users/{username}/convert-type admin adminConvertUserType
|
||||
// ---
|
||||
// summary: Convert an account between the user and bot types
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user to convert
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ConvertUserTypeOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
targetType, err := convert.UserTypeFromString(web.GetForm[*api.ConvertUserTypeOption](ctx).UserType)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.ContextUser, &user_service.UpdateOptions{UserType: optional.Some(targetType)}); err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -1905,7 +1905,6 @@ func Routes() *web.Router {
|
||||
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
|
||||
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
|
||||
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
|
||||
m.Post("/convert-type", bind(api.ConvertUserTypeOption{}), admin.ConvertUserType)
|
||||
m.Get("/badges", admin.ListUserBadges)
|
||||
m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
|
||||
m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
|
||||
|
||||
@@ -59,9 +59,6 @@ type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
RenameUserOption api.RenameUserOption
|
||||
|
||||
// in:body
|
||||
ConvertUserTypeOption api.ConvertUserTypeOption
|
||||
|
||||
// in:body
|
||||
CreateLabelOption api.CreateLabelOption
|
||||
// in:body
|
||||
|
||||
@@ -506,9 +506,6 @@ func EditUserPost(ctx *context.Context) {
|
||||
case errors.Is(err, user_model.ErrBotCanNotBeAdmin):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.convert_type.admin_not_allowed"))
|
||||
ctx.Redirect(userLink)
|
||||
case errors.Is(err, user_model.ErrUserTypeCanNotConvert):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.convert_type.not_convertible"))
|
||||
ctx.Redirect(userLink)
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.Flash.Error(err.Error())
|
||||
ctx.Redirect(userLink)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
func userTypeToString(t user_model.UserType) api.UserTypeString {
|
||||
func UserTypeToString(t user_model.UserType) api.UserTypeString {
|
||||
switch t {
|
||||
case user_model.UserTypeOrganization, user_model.UserTypeOrganizationReserved:
|
||||
return api.UserTypeStringOrganization
|
||||
@@ -74,7 +74,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
|
||||
result := &api.User{
|
||||
ID: user.ID,
|
||||
UserName: user.Name,
|
||||
Type: userTypeToString(user.Type),
|
||||
Type: UserTypeToString(user.Type),
|
||||
FullName: user.FullName,
|
||||
Email: user.GetPlaceholderEmail(),
|
||||
AvatarURL: user.AvatarLink(ctx),
|
||||
|
||||
+21
-72
@@ -3707,25 +3707,6 @@
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"ConvertUserTypeOption": {
|
||||
"description": "ConvertUserTypeOption options when converting a user between individual and bot",
|
||||
"properties": {
|
||||
"user_type": {
|
||||
"description": "The target user type",
|
||||
"enum": [
|
||||
"User",
|
||||
"Bot"
|
||||
],
|
||||
"type": "string",
|
||||
"x-go-name": "UserType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"user_type"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"CreateAccessTokenOption": {
|
||||
"description": "CreateAccessTokenOption options when create access token",
|
||||
"properties": {
|
||||
@@ -6330,6 +6311,14 @@
|
||||
"type": "integer",
|
||||
"x-go-name": "SourceID"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserTypeString"
|
||||
}
|
||||
],
|
||||
"description": "The user type"
|
||||
},
|
||||
"visibility": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -10742,15 +10731,12 @@
|
||||
"x-go-name": "StarredRepos"
|
||||
},
|
||||
"type": {
|
||||
"description": "the account type",
|
||||
"enum": [
|
||||
"User",
|
||||
"Organization",
|
||||
"Bot"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserTypeString"
|
||||
}
|
||||
],
|
||||
"type": "string",
|
||||
"x-go-enum-desc": "User UserTypeStringUser\nOrganization UserTypeStringOrganization\nBot UserTypeStringBot",
|
||||
"x-go-name": "Type"
|
||||
"description": "the user type"
|
||||
},
|
||||
"visibility": {
|
||||
"allOf": [
|
||||
@@ -10908,6 +10894,14 @@
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"UserTypeString": {
|
||||
"enum": [
|
||||
"User",
|
||||
"Organization",
|
||||
"Bot"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"VisibilityString": {
|
||||
"enum": [
|
||||
"public",
|
||||
@@ -12291,51 +12285,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/users/{username}/convert-type": {
|
||||
"post": {
|
||||
"operationId": "adminConvertUserType",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "username of the user to convert",
|
||||
"in": "path",
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConvertUserTypeOption"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-originalParamName": "body"
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/components/responses/empty"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/notFound"
|
||||
}
|
||||
},
|
||||
"summary": "Convert an account between the user and bot types",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/users/{username}/keys": {
|
||||
"post": {
|
||||
"operationId": "adminCreatePublicKey",
|
||||
|
||||
+12
-66
@@ -1144,52 +1144,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/users/{username}/convert-type": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Convert an account between the user and bot types",
|
||||
"operationId": "adminConvertUserType",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user to convert",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ConvertUserTypeOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/users/{username}/keys": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -26790,25 +26744,6 @@
|
||||
},
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"ConvertUserTypeOption": {
|
||||
"description": "ConvertUserTypeOption options when converting a user between individual and bot",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"user_type"
|
||||
],
|
||||
"properties": {
|
||||
"user_type": {
|
||||
"description": "The target user type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"User",
|
||||
"Bot"
|
||||
],
|
||||
"x-go-name": "UserType"
|
||||
}
|
||||
},
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"CreateAccessTokenOption": {
|
||||
"description": "CreateAccessTokenOption options when create access token",
|
||||
"type": "object",
|
||||
@@ -29457,6 +29392,17 @@
|
||||
"format": "int64",
|
||||
"x-go-name": "SourceID"
|
||||
},
|
||||
"type": {
|
||||
"description": "The user type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"User",
|
||||
"Organization",
|
||||
"Bot"
|
||||
],
|
||||
"x-go-enum-desc": "User UserTypeStringUser\nOrganization UserTypeStringOrganization\nBot UserTypeStringBot",
|
||||
"x-go-name": "Type"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "User visibility level: public, limited, or private",
|
||||
"type": "string",
|
||||
@@ -33820,7 +33766,7 @@
|
||||
"x-go-name": "StarredRepos"
|
||||
},
|
||||
"type": {
|
||||
"description": "the account type",
|
||||
"description": "the user type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"User",
|
||||
|
||||
@@ -259,7 +259,9 @@ func TestAdminBotUser(t *testing.T) {
|
||||
}), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user4/convert-type", map[string]string{"user_type": "Bot"}).AddBasicAuth("user1"), http.StatusNoContent)
|
||||
MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/org3", map[string]string{"type": "Organization"}).AddBasicAuth("user1"), http.StatusOK)
|
||||
MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/user4", map[string]string{"type": "Bot", "password": "Bot-Password-1234"}).AddBasicAuth("user1"), http.StatusBadRequest)
|
||||
MakeRequest(t, NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/user4", map[string]string{"type": "Bot"}).AddBasicAuth("user1"), http.StatusOK)
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
assert.True(t, user4.IsTypeBot())
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user4"), http.StatusOK)
|
||||
|
||||
Reference in New Issue
Block a user