mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-22 04:33:42 +09:00
feat: manage bot accounts from the admin UI, API and CLI (#38966)
Adds first-class bot accounts (`UserTypeBot`): local, password-less
users for automation that authenticate only with access tokens.
1. Admin UI: create bots, filter users by type, manage a bot's access
tokens, convert between user and bot
2. API: `POST /admin/users/{username}/convert-type`, and user objects
gain a GitHub-compatible `type` (`User`, `Organization`, `Bot`)
3. CLI: `gitea admin user change-type`, `--user-type` accepts `User` or
`Bot` case-insensitively
4. Converting keeps the password, 2FA, OAuth2 grants and access tokens,
and since sign-in rejects bots, converting back restores the account.
Only local, non-admin accounts can be converted, and conversions are
audited
5. Session, reverse proxy, SSPI, external source and password reset
sign-in reject non-individual users, so a bot never gets an interactive
session
6. Bots receive no notifications or emails
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: joestump <joe@joestump.net>
Co-authored-by: Joe Stump <joe@stu.mp>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
co-authored by
Nicolas
joestump
Joe Stump
silverwind
Lunny Xiao
parent
db7dbd5a6b
commit
3bec08f998
@@ -206,7 +206,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
|
||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||
default:
|
||||
ctx.APIErrorInternal(err)
|
||||
ctx.APIErrorAuto(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
if user_model.IsErrDeleteLastAdminUser(err) {
|
||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
ctx.APIErrorAuto(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -552,3 +552,46 @@ 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,6 +1905,7 @@ 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,6 +59,9 @@ type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
RenameUserOption api.RenameUserOption
|
||||
|
||||
// in:body
|
||||
ConvertUserTypeOption api.ConvertUserTypeOption
|
||||
|
||||
// in:body
|
||||
CreateLabelOption api.CreateLabelOption
|
||||
// in:body
|
||||
|
||||
@@ -25,9 +25,8 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
sortOrder := ctx.FormString("sort", UserSearchDefaultAdminSort)
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization, user_model.UserTypeOrganizationReserved},
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
},
|
||||
|
||||
+91
-17
@@ -22,6 +22,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"gitea.dev/services/audit"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/mailer"
|
||||
org_service "gitea.dev/services/org"
|
||||
@@ -60,6 +62,15 @@ func Users(ctx *context.Context) {
|
||||
}
|
||||
|
||||
sortType := ctx.FormString("sort", UserSearchDefaultAdminSort)
|
||||
|
||||
// unfiltered, an administrator needs to list every account kind
|
||||
types := []user_model.UserType{user_model.UserTypeIndividual, user_model.UserTypeUserReserved, user_model.UserTypeBot, user_model.UserTypeRemoteUser}
|
||||
userTypeFilter := api.UserTypeString(ctx.FormString("user_type"))
|
||||
ctx.Data["UserTypeFilter"] = ""
|
||||
if t, err := convert.UserTypeFromString(userTypeFilter); err == nil {
|
||||
types, ctx.Data["UserTypeFilter"] = []user_model.UserType{t}, userTypeFilter
|
||||
}
|
||||
|
||||
ctx.PageData["adminUserListSearchForm"] = map[string]any{
|
||||
"StatusFilterMap": statusFilterMap,
|
||||
"SortType": sortType,
|
||||
@@ -67,7 +78,7 @@ func Users(ctx *context.Context) {
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
Types: types,
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
@@ -77,7 +88,6 @@ func Users(ctx *context.Context) {
|
||||
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
|
||||
OrderBy: db.SearchOrderBy(sortType),
|
||||
}, tplUsers)
|
||||
}
|
||||
@@ -90,6 +100,7 @@ func NewUser(ctx *context.Context) {
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
|
||||
ctx.Data["login_type"] = "0-0"
|
||||
ctx.Data["user_type"] = api.UserTypeStringUser
|
||||
|
||||
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
|
||||
IsActive: optional.Some(true),
|
||||
@@ -140,7 +151,10 @@ func NewUserPost(ctx *context.Context) {
|
||||
Visibility: &form.Visibility,
|
||||
}
|
||||
|
||||
if len(form.LoginType) > 0 {
|
||||
if form.UserType == api.UserTypeStringBot {
|
||||
u.Type = user_model.UserTypeBot
|
||||
u.Passwd = ""
|
||||
} else if len(form.LoginType) > 0 {
|
||||
fields := strings.Split(form.LoginType, "-")
|
||||
if len(fields) == 2 {
|
||||
lType, _ := strconv.ParseInt(fields[0], 10, 0)
|
||||
@@ -149,7 +163,7 @@ func NewUserPost(ctx *context.Context) {
|
||||
u.LoginName = form.LoginName
|
||||
}
|
||||
}
|
||||
if u.LoginType == auth.NoType || u.LoginType == auth.Plain {
|
||||
if !u.IsTypeBot() && (u.LoginType == auth.NoType || u.LoginType == auth.Plain) {
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
|
||||
@@ -260,6 +274,7 @@ func prepareUserInfo(ctx *context.Context) *user_model.User {
|
||||
return nil
|
||||
}
|
||||
ctx.Data["TwoFactorEnabled"] = hasTOTP || hasWebAuthn
|
||||
ctx.Data["CanConvertUserType"] = user_service.CheckConvertUserType(u) == nil
|
||||
|
||||
return u
|
||||
}
|
||||
@@ -305,9 +320,42 @@ func ViewUser(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if u.IsTypeBot() {
|
||||
ctx.Data["BotAccessTokens"] = user_setting.NewAccessTokensPanel(ctx, u, ctx.Link+"/access_tokens")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserView)
|
||||
}
|
||||
|
||||
// getTargetBot loads the bot whose tokens an admin manages, other accounts manage their own
|
||||
func getTargetBot(ctx *context.Context) *user_model.User {
|
||||
u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetUserByID", user_model.IsErrUserNotExist, err)
|
||||
return nil
|
||||
}
|
||||
if !u.IsTypeBot() {
|
||||
ctx.JSONError(ctx.Tr("admin.users.bot_token_only"))
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func NewBotTokenPost(ctx *context.Context) {
|
||||
if u := getTargetBot(ctx); u != nil {
|
||||
user_setting.CreateAccessToken(ctx, u)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteBotToken(ctx *context.Context) {
|
||||
if u := getTargetBot(ctx); u != nil {
|
||||
user_setting.DeleteAccessToken(ctx, u)
|
||||
}
|
||||
}
|
||||
|
||||
func editUserCommon(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
@@ -344,6 +392,8 @@ func EditUserPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
userLink := setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))
|
||||
|
||||
if form.UserName != "" {
|
||||
if err := user_service.RenameUser(ctx, u, form.UserName, ctx.Doer); err != nil {
|
||||
switch {
|
||||
@@ -369,9 +419,19 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
authOpts := &user_service.UpdateAuthOptions{
|
||||
Password: optional.FromNonDefault(form.Password),
|
||||
LoginName: optional.Some(form.LoginName),
|
||||
userType := u.Type
|
||||
if formUserType, err := convert.UserTypeFromString(form.UserType); err == nil {
|
||||
userType = formUserType
|
||||
}
|
||||
|
||||
authOpts := &user_service.UpdateAuthOptions{}
|
||||
if !u.IsTypeBot() && userType != user_model.UserTypeBot { // the auth fields hidden for bots still submit their values
|
||||
authOpts.Password = optional.FromNonDefault(form.Password)
|
||||
authOpts.LoginName = optional.Some(form.LoginName)
|
||||
if fields := strings.Split(form.LoginType, "-"); len(fields) == 2 {
|
||||
authSource, _ := strconv.ParseInt(fields[1], 10, 64)
|
||||
authOpts.LoginSource = optional.Some(authSource)
|
||||
}
|
||||
}
|
||||
|
||||
// skip self Prohibit Login
|
||||
@@ -381,13 +441,6 @@ func EditUserPost(ctx *context.Context) {
|
||||
authOpts.ProhibitLogin = optional.Some(form.ProhibitLogin)
|
||||
}
|
||||
|
||||
fields := strings.Split(form.LoginType, "-")
|
||||
if len(fields) == 2 {
|
||||
authSource, _ := strconv.ParseInt(fields[1], 10, 64)
|
||||
|
||||
authOpts.LoginSource = optional.Some(authSource)
|
||||
}
|
||||
|
||||
if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
@@ -402,6 +455,9 @@ func EditUserPost(ctx *context.Context) {
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.Flash.Error(err.Error())
|
||||
ctx.Redirect(userLink)
|
||||
default:
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
@@ -440,16 +496,28 @@ func EditUserPost(ctx *context.Context) {
|
||||
IsRestricted: optional.Some(form.Restricted),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
Language: optional.Some(form.Language),
|
||||
UserType: optional.Some(userType),
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
if user_model.IsErrDeleteLastAdminUser(err) {
|
||||
switch {
|
||||
case user_model.IsErrDeleteLastAdminUser(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
|
||||
} else {
|
||||
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)
|
||||
default:
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
if form.Reset2FA {
|
||||
@@ -460,7 +528,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
ctx.Redirect(userLink)
|
||||
}
|
||||
|
||||
func ImpersonateUser(ctx *context.Context) {
|
||||
@@ -469,6 +537,12 @@ func ImpersonateUser(ctx *context.Context) {
|
||||
ctx.JSONError("unable to get user")
|
||||
return
|
||||
}
|
||||
|
||||
if u.IsTypeBot() {
|
||||
ctx.JSONError(ctx.Tr("admin.users.impersonate_bot_not_allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
err = auth_service.ImpersonateUser(ctx.Session, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("unable to impersonate user", err)
|
||||
|
||||
@@ -62,6 +62,9 @@ func ForgotPasswdPost(ctx *context.Context) {
|
||||
ctx.Data["Email"] = email
|
||||
|
||||
u, err := user_model.GetUserByEmail(ctx, email)
|
||||
if err == nil && !u.IsIndividual() {
|
||||
err = user_model.ErrUserNotExist{}
|
||||
}
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
const (
|
||||
tplSettingsApplications templates.TplName = "user/settings/applications"
|
||||
tplAccessTokens templates.TplName = "shared/user/access_tokens"
|
||||
)
|
||||
|
||||
// Applications render manage access token page
|
||||
@@ -34,11 +35,40 @@ func Applications(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
||||
}
|
||||
|
||||
// ApplicationsPost response for add user's access token
|
||||
func ApplicationsPost(ctx *context.Context) {
|
||||
form := web.GetForm[*forms.NewAccessTokenForm](ctx)
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
type AccessTokensPanel struct {
|
||||
Tokens []*auth_model.AccessToken
|
||||
ScopeCategories []string
|
||||
ScopePublicOnly auth_model.AccessTokenScope
|
||||
Link string
|
||||
IsBot bool
|
||||
NewTokenValue string
|
||||
}
|
||||
|
||||
func NewAccessTokensPanel(ctx *context.Context, owner *user_model.User, link string) *AccessTokensPanel {
|
||||
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: owner.ID})
|
||||
if err != nil {
|
||||
ctx.ServerError("ListAccessTokens", err)
|
||||
return nil
|
||||
}
|
||||
panel := &AccessTokensPanel{
|
||||
Tokens: tokens,
|
||||
ScopeCategories: auth_model.GetAccessTokenCategories(),
|
||||
ScopePublicOnly: auth_model.AccessTokenScopePublicOnly,
|
||||
Link: link,
|
||||
IsBot: owner.IsTypeBot(),
|
||||
}
|
||||
if !owner.IsAdmin {
|
||||
panel.ScopeCategories = util.SliceRemoveAll(panel.ScopeCategories, "admin")
|
||||
}
|
||||
return panel
|
||||
}
|
||||
|
||||
// CreateAccessToken handles the panel's create form, which posts to the panel link
|
||||
func CreateAccessToken(ctx *context.Context, owner *user_model.User) {
|
||||
form := context.GetFetchActionForm[*forms.NewAccessTokenForm](ctx)
|
||||
if form == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
var scopeNames []string
|
||||
@@ -55,17 +85,12 @@ func ApplicationsPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if !scope.HasPermissionScope() {
|
||||
ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
loadApplicationsData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
||||
ctx.JSONError(ctx.Tr("settings.at_least_one_permission"))
|
||||
return
|
||||
}
|
||||
|
||||
t := &auth_model.AccessToken{
|
||||
UID: ctx.Doer.ID,
|
||||
UID: owner.ID,
|
||||
Name: form.Name,
|
||||
Scope: scope,
|
||||
}
|
||||
@@ -76,8 +101,7 @@ func ApplicationsPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
ctx.Flash.Error(ctx.Tr("settings.generate_token_name_duplicate", t.Name))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
|
||||
ctx.JSONErrorWithField(ctx.Tr("settings.generate_token_name_duplicate", t.Name), "name")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,28 +130,41 @@ func ApplicationsPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserAccessTokenAdd, ctx.Doer, "token", t.Name, "token_scope", t.Scope)
|
||||
audit.Record(ctx, audit_model.UserAccessTokenAdd, owner, "token", t.Name, "token_scope", t.Scope)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
|
||||
ctx.Flash.Info(t.Token)
|
||||
panel := NewAccessTokensPanel(ctx, owner, ctx.Link)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
panel.NewTokenValue = t.Token
|
||||
if err := ctx.Render.HTML(ctx.Resp, http.StatusOK, tplAccessTokens, panel, ctx.TemplateContext); err != nil {
|
||||
ctx.ServerError("Render", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
|
||||
// ApplicationsPost response for add user's access token
|
||||
func ApplicationsPost(ctx *context.Context) {
|
||||
CreateAccessToken(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// DeleteApplication response for delete user access token
|
||||
func DeleteApplication(ctx *context.Context) {
|
||||
t, err := auth_model.GetAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID)
|
||||
DeleteAccessToken(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func DeleteAccessToken(ctx *context.Context, owner *user_model.User) {
|
||||
t, err := auth_model.GetAccessTokenByID(ctx, ctx.FormInt64("id"), owner.ID)
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetAccessTokenByID: " + err.Error())
|
||||
} else if err := auth_model.DeleteAccessTokenByID(ctx, t.ID, ctx.Doer.ID); err != nil {
|
||||
} else if err := auth_model.DeleteAccessTokenByID(ctx, t.ID, owner.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserAccessTokenRemove, ctx.Doer, "token", t.Name)
|
||||
audit.Record(ctx, audit_model.UserAccessTokenRemove, owner, "token", t.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications")
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
// RegenerateAccessToken response for regenerating a user's access token
|
||||
@@ -143,23 +180,14 @@ func RegenerateAccessToken(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func loadApplicationsData(ctx *context.Context) {
|
||||
ctx.Data["AccessTokenScopePublicOnly"] = auth_model.AccessTokenScopePublicOnly
|
||||
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
|
||||
if err != nil {
|
||||
ctx.ServerError("ListAccessTokens", err)
|
||||
ctx.Data["AccessTokens"] = NewAccessTokensPanel(ctx, ctx.Doer, ctx.Link)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["Tokens"] = tokens
|
||||
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
|
||||
|
||||
// Handle specific ordered token categories for admin or non-admin users
|
||||
tokenCategoryNames := auth_model.GetAccessTokenCategories()
|
||||
if !ctx.Doer.IsAdmin {
|
||||
tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin")
|
||||
}
|
||||
ctx.Data["TokenCategories"] = tokenCategoryNames
|
||||
|
||||
if setting.OAuth2.Enabled {
|
||||
var err error
|
||||
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
})
|
||||
|
||||
+3
-1
@@ -695,7 +695,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
// access token applications
|
||||
m.Combo("").Get(user_setting.Applications).
|
||||
Post(web.Bind[*forms.NewAccessTokenForm](), user_setting.ApplicationsPost)
|
||||
Post(user_setting.ApplicationsPost)
|
||||
m.Post("/delete", user_setting.DeleteApplication)
|
||||
m.Post("/regenerate", user_setting.RegenerateAccessToken)
|
||||
})
|
||||
@@ -824,6 +824,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/{userid}/delete", admin.DeleteUser)
|
||||
m.Post("/{userid}/avatar", web.Bind[*forms.AvatarForm](), admin.AvatarPost)
|
||||
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
|
||||
m.Post("/{userid}/access_tokens", admin.NewBotTokenPost)
|
||||
m.Post("/{userid}/access_tokens/delete", admin.DeleteBotToken)
|
||||
m.Post("/{userid}/orgs/{org_id}/remove", admin.RemoveUserFromOrg)
|
||||
m.Post("/{userid}/orgs/remove-all", admin.RemoveUserFromAllOrgs)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user