Allow creation of OAuth2 applications for orgs (#18084)
Adds the settings pages to create OAuth2 apps also to the org settings and allows to create apps for orgs. Refactoring: the oauth2 related templates are shared for instance-wide/org/user, and the backend code uses `OAuth2CommonHandlers` to share code for instance-wide/org/user. Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>tokarchuk/v1.18
parent
97f3f1988b
commit
a813c9d8f3
File diff suppressed because one or more lines are too long
@ -0,0 +1,93 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package org |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"code.gitea.io/gitea/models/auth" |
||||
"code.gitea.io/gitea/modules/base" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting" |
||||
) |
||||
|
||||
const ( |
||||
tplSettingsApplications base.TplName = "org/settings/applications" |
||||
tplSettingsOAuthApplicationEdit base.TplName = "org/settings/applications_oauth2_edit" |
||||
) |
||||
|
||||
func newOAuth2CommonHandlers(org *context.Organization) *user_setting.OAuth2CommonHandlers { |
||||
return &user_setting.OAuth2CommonHandlers{ |
||||
OwnerID: org.Organization.ID, |
||||
BasePathList: fmt.Sprintf("%s/org/%s/settings/applications", setting.AppSubURL, org.Organization.Name), |
||||
BasePathEditPrefix: fmt.Sprintf("%s/org/%s/settings/applications/oauth2", setting.AppSubURL, org.Organization.Name), |
||||
TplAppEdit: tplSettingsOAuthApplicationEdit, |
||||
} |
||||
} |
||||
|
||||
// Applications render org applications page (for org, at the moment, there are only OAuth2 applications)
|
||||
func Applications(ctx *context.Context) { |
||||
ctx.Data["Title"] = ctx.Tr("settings.applications") |
||||
ctx.Data["PageIsOrgSettings"] = true |
||||
ctx.Data["PageIsSettingsApplications"] = true |
||||
|
||||
apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, ctx.Org.Organization.ID) |
||||
if err != nil { |
||||
ctx.ServerError("GetOAuth2ApplicationsByUserID", err) |
||||
return |
||||
} |
||||
ctx.Data["Applications"] = apps |
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications) |
||||
} |
||||
|
||||
// OAuthApplicationsPost response for adding an oauth2 application
|
||||
func OAuthApplicationsPost(ctx *context.Context) { |
||||
ctx.Data["Title"] = ctx.Tr("settings.applications") |
||||
ctx.Data["PageIsOrgSettings"] = true |
||||
ctx.Data["PageIsSettingsApplications"] = true |
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org) |
||||
oa.AddApp(ctx) |
||||
} |
||||
|
||||
// OAuth2ApplicationShow displays the given application
|
||||
func OAuth2ApplicationShow(ctx *context.Context) { |
||||
ctx.Data["PageIsOrgSettings"] = true |
||||
ctx.Data["PageIsSettingsApplications"] = true |
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org) |
||||
oa.EditShow(ctx) |
||||
} |
||||
|
||||
// OAuth2ApplicationEdit response for editing oauth2 application
|
||||
func OAuth2ApplicationEdit(ctx *context.Context) { |
||||
ctx.Data["Title"] = ctx.Tr("settings.applications") |
||||
ctx.Data["PageIsOrgSettings"] = true |
||||
ctx.Data["PageIsSettingsApplications"] = true |
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org) |
||||
oa.EditSave(ctx) |
||||
} |
||||
|
||||
// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
|
||||
func OAuthApplicationsRegenerateSecret(ctx *context.Context) { |
||||
ctx.Data["Title"] = ctx.Tr("settings") |
||||
ctx.Data["PageIsOrgSettings"] = true |
||||
ctx.Data["PageIsSettingsApplications"] = true |
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org) |
||||
oa.RegenerateSecret(ctx) |
||||
} |
||||
|
||||
// DeleteOAuth2Application deletes the given oauth2 application
|
||||
func DeleteOAuth2Application(ctx *context.Context) { |
||||
oa := newOAuth2CommonHandlers(ctx.Org) |
||||
oa.DeleteApp(ctx) |
||||
} |
||||
|
||||
// TODO: revokes the grant with the given id
|
@ -0,0 +1,150 @@ |
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package setting |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"code.gitea.io/gitea/models/auth" |
||||
"code.gitea.io/gitea/modules/base" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/web" |
||||
"code.gitea.io/gitea/services/forms" |
||||
) |
||||
|
||||
type OAuth2CommonHandlers struct { |
||||
OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID
|
||||
BasePathList string // the base URL for the application list page, eg: "/user/setting/applications"
|
||||
BasePathEditPrefix string // the base URL for the application edit page, will be appended with app id, eg: "/user/setting/applications/oauth2"
|
||||
TplAppEdit base.TplName // the template for the application edit page
|
||||
} |
||||
|
||||
func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) { |
||||
app := ctx.Data["App"].(*auth.OAuth2Application) |
||||
ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID) |
||||
ctx.HTML(http.StatusOK, oa.TplAppEdit) |
||||
} |
||||
|
||||
// AddApp adds an oauth2 application
|
||||
func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) { |
||||
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm) |
||||
if ctx.HasError() { |
||||
// go to the application list page
|
||||
ctx.Redirect(oa.BasePathList) |
||||
return |
||||
} |
||||
|
||||
// TODO validate redirect URI
|
||||
app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{ |
||||
Name: form.Name, |
||||
RedirectURIs: []string{form.RedirectURI}, |
||||
UserID: oa.OwnerID, |
||||
}) |
||||
if err != nil { |
||||
ctx.ServerError("CreateOAuth2Application", err) |
||||
return |
||||
} |
||||
|
||||
// render the edit page with secret
|
||||
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"), true) |
||||
ctx.Data["App"] = app |
||||
ctx.Data["ClientSecret"], err = app.GenerateClientSecret() |
||||
if err != nil { |
||||
ctx.ServerError("GenerateClientSecret", err) |
||||
return |
||||
} |
||||
oa.renderEditPage(ctx) |
||||
} |
||||
|
||||
// EditShow displays the given application
|
||||
func (oa *OAuth2CommonHandlers) EditShow(ctx *context.Context) { |
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id")) |
||||
if err != nil { |
||||
if auth.IsErrOAuthApplicationNotFound(err) { |
||||
ctx.NotFound("Application not found", err) |
||||
return |
||||
} |
||||
ctx.ServerError("GetOAuth2ApplicationByID", err) |
||||
return |
||||
} |
||||
if app.UID != oa.OwnerID { |
||||
ctx.NotFound("Application not found", nil) |
||||
return |
||||
} |
||||
ctx.Data["App"] = app |
||||
oa.renderEditPage(ctx) |
||||
} |
||||
|
||||
// EditSave saves the oauth2 application
|
||||
func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) { |
||||
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm) |
||||
|
||||
if ctx.HasError() { |
||||
oa.renderEditPage(ctx) |
||||
return |
||||
} |
||||
|
||||
// TODO validate redirect URI
|
||||
var err error |
||||
if ctx.Data["App"], err = auth.UpdateOAuth2Application(auth.UpdateOAuth2ApplicationOptions{ |
||||
ID: ctx.ParamsInt64("id"), |
||||
Name: form.Name, |
||||
RedirectURIs: []string{form.RedirectURI}, |
||||
UserID: oa.OwnerID, |
||||
}); err != nil { |
||||
ctx.ServerError("UpdateOAuth2Application", err) |
||||
return |
||||
} |
||||
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success")) |
||||
ctx.Redirect(oa.BasePathList) |
||||
} |
||||
|
||||
// RegenerateSecret regenerates the secret
|
||||
func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) { |
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id")) |
||||
if err != nil { |
||||
if auth.IsErrOAuthApplicationNotFound(err) { |
||||
ctx.NotFound("Application not found", err) |
||||
return |
||||
} |
||||
ctx.ServerError("GetOAuth2ApplicationByID", err) |
||||
return |
||||
} |
||||
if app.UID != oa.OwnerID { |
||||
ctx.NotFound("Application not found", nil) |
||||
return |
||||
} |
||||
ctx.Data["App"] = app |
||||
ctx.Data["ClientSecret"], err = app.GenerateClientSecret() |
||||
if err != nil { |
||||
ctx.ServerError("GenerateClientSecret", err) |
||||
return |
||||
} |
||||
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"), true) |
||||
oa.renderEditPage(ctx) |
||||
} |
||||
|
||||
// DeleteApp deletes the given oauth2 application
|
||||
func (oa *OAuth2CommonHandlers) DeleteApp(ctx *context.Context) { |
||||
if err := auth.DeleteOAuth2Application(ctx.ParamsInt64("id"), oa.OwnerID); err != nil { |
||||
ctx.ServerError("DeleteOAuth2Application", err) |
||||
return |
||||
} |
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success")) |
||||
ctx.JSON(http.StatusOK, map[string]interface{}{"redirect": oa.BasePathList}) |
||||
} |
||||
|
||||
// RevokeGrant revokes the grant
|
||||
func (oa *OAuth2CommonHandlers) RevokeGrant(ctx *context.Context) { |
||||
if err := auth.RevokeOAuth2Grant(ctx, ctx.ParamsInt64("grantId"), oa.OwnerID); err != nil { |
||||
ctx.ServerError("RevokeOAuth2Grant", err) |
||||
return |
||||
} |
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success")) |
||||
ctx.JSON(http.StatusOK, map[string]interface{}{"redirect": oa.BasePathList}) |
||||
} |
@ -0,0 +1,18 @@ |
||||
{{template "base/head" .}} |
||||
<div class="page-content organization settings options"> |
||||
{{template "org/header" .}} |
||||
<div class="ui container"> |
||||
<div class="ui grid"> |
||||
{{template "org/settings/navbar" .}} |
||||
<div class="twelve wide column content"> |
||||
{{template "base/alert" .}} |
||||
<h4 class="ui top attached header"> |
||||
{{.locale.Tr "settings.applications"}} |
||||
</h4> |
||||
|
||||
{{template "user/settings/applications_oauth2_list" .}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{template "base/footer" .}} |
@ -0,0 +1,7 @@ |
||||
{{template "base/head" .}} |
||||
<div class="page-content organization settings options"> |
||||
{{template "org/header" .}} |
||||
|
||||
{{template "user/settings/applications_oauth2_edit_form" .}} |
||||
</div> |
||||
{{template "base/footer" .}} |
@ -1,59 +1,6 @@ |
||||
<h4 class="ui top attached header"> |
||||
{{.locale.Tr "settings.manage_oauth2_applications"}} |
||||
</h4> |
||||
<div class="ui attached segment"> |
||||
<div class="ui key list"> |
||||
<div class="item"> |
||||
{{.locale.Tr "settings.oauth2_application_create_description"}} |
||||
</div> |
||||
{{range $app := .Applications}} |
||||
<div class="item"> |
||||
<div class="right floated content"> |
||||
<a href="{{$.Link}}/oauth2/{{$app.ID}}" class="ui primary tiny button"> |
||||
{{svg "octicon-pencil" 16 "mr-2"}} |
||||
{{$.locale.Tr "settings.oauth2_application_edit"}} |
||||
</a> |
||||
<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" |
||||
data-url="{{AppSubUrl}}/user/settings/applications/oauth2/delete" |
||||
data-id="{{$app.ID}}"> |
||||
{{svg "octicon-trash" 16 "mr-2"}} |
||||
{{$.locale.Tr "settings.delete_key"}} |
||||
</button> |
||||
</div> |
||||
<div class="content"> |
||||
<strong>{{$app.Name}}</strong> |
||||
</div> |
||||
</div> |
||||
{{end}} |
||||
</div> |
||||
</div> |
||||
<div class="ui attached bottom segment"> |
||||
<h5 class="ui top header"> |
||||
{{.locale.Tr "settings.create_oauth2_application"}} |
||||
</h5> |
||||
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post"> |
||||
{{.CsrfTokenHtml}} |
||||
<div class="field {{if .Err_AppName}}error{{end}}"> |
||||
<label for="application-name">{{.locale.Tr "settings.oauth2_application_name"}}</label> |
||||
<input id="application-name" name="application_name" value="{{.application_name}}" required> |
||||
</div> |
||||
<div class="field {{if .Err_RedirectURI}}error{{end}}"> |
||||
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label> |
||||
<input type="url" name="redirect_uri" id="redirect-uri"> |
||||
</div> |
||||
<button class="ui green button"> |
||||
{{.locale.Tr "settings.create_oauth2_application_button"}} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
|
||||
<div class="ui small basic delete modal" id="remove-gitea-oauth2-application"> |
||||
<div class="ui icon header"> |
||||
{{svg "octicon-trash"}} |
||||
{{.locale.Tr "settings.remove_oauth2_application"}} |
||||
</div> |
||||
<div class="content"> |
||||
<p>{{.locale.Tr "settings.oauth2_application_remove_description"}}</p> |
||||
</div> |
||||
{{template "base/delete_modal_actions" .}} |
||||
</div> |
||||
{{template "user/settings/applications_oauth2_list" .}} |
||||
|
||||
|
@ -1,68 +1,8 @@ |
||||
{{template "base/head" .}} |
||||
<div class="page-content user settings applications"> |
||||
{{template "user/settings/navbar" .}} |
||||
<div class="ui container"> |
||||
{{template "base/alert" .}} |
||||
<h4 class="ui top attached header"> |
||||
{{.locale.Tr "settings.edit_oauth2_application"}} |
||||
</h4> |
||||
<div class="ui attached segment"> |
||||
<p>{{.locale.Tr "settings.oauth2_application_create_description"}}</p> |
||||
</div> |
||||
<div class="ui attached segment form ignore-dirty"> |
||||
{{.CsrfTokenHtml}} |
||||
<div class="field"> |
||||
<label for="client-id">{{.locale.Tr "settings.oauth2_client_id"}}</label> |
||||
<input id="client-id" readonly value="{{.App.ClientID}}"> |
||||
</div> |
||||
{{if .ClientSecret}} |
||||
<div class="field"> |
||||
<label for="client-secret">{{.locale.Tr "settings.oauth2_client_secret"}}</label> |
||||
<input id="client-secret" type="text" readonly value="{{.ClientSecret}}"> |
||||
</div> |
||||
{{else}} |
||||
<div class="field"> |
||||
<label for="client-secret">{{.locale.Tr "settings.oauth2_client_secret"}}</label> |
||||
<input id="client-secret" type="password" readonly value="averysecuresecret"> |
||||
</div> |
||||
{{end}} |
||||
<div class="item"> |
||||
<!-- TODO add regenerate secret functionality */ --> |
||||
{{.locale.Tr "settings.oauth2_regenerate_secret_hint"}} |
||||
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/settings/applications/oauth2/{{.App.ID}}/regenerate_secret" method="post"> |
||||
{{.CsrfTokenHtml}} |
||||
<a href="#" onclick="event.target.parentNode.submit()">{{.locale.Tr "settings.oauth2_regenerate_secret"}}</a> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
<div class="ui attached bottom segment"> |
||||
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/settings/applications/oauth2/{{.App.ID}}" method="post"> |
||||
{{.CsrfTokenHtml}} |
||||
<div class="field {{if .Err_AppName}}error{{end}}"> |
||||
<label for="application-name">{{.locale.Tr "settings.oauth2_application_name"}}</label> |
||||
<input id="application-name" value="{{.App.Name}}" name="application_name" required> |
||||
</div> |
||||
<div class="field {{if .Err_RedirectURI}}error{{end}}"> |
||||
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label> |
||||
<input type="url" name="redirect_uri" value="{{.App.PrimaryRedirectURI}}" id="redirect-uri"> |
||||
</div> |
||||
<button class="ui green button"> |
||||
{{.locale.Tr "settings.save_application"}} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="ui small basic delete modal" id="delete-oauth2-application"> |
||||
<div class="ui icon header"> |
||||
{{svg "octicon-trash"}} |
||||
{{.locale.Tr "settings.remove_oauth2_application"}} |
||||
</div> |
||||
<div class="content"> |
||||
<p>{{.locale.Tr "settings.remove_oauth2_application_desc"}}</p> |
||||
</div> |
||||
{{template "base/delete_modal_actions" .}} |
||||
{{template "user/settings/applications_oauth2_edit_form" .}} |
||||
</div> |
||||
|
||||
{{template "base/footer" .}} |
||||
|
@ -0,0 +1,51 @@ |
||||
<div class="ui container"> |
||||
{{template "base/alert" .}} |
||||
<h4 class="ui top attached header"> |
||||
{{.locale.Tr "settings.edit_oauth2_application"}} |
||||
</h4> |
||||
<div class="ui attached segment"> |
||||
<p>{{.locale.Tr "settings.oauth2_application_create_description"}}</p> |
||||
</div> |
||||
<div class="ui attached segment form ignore-dirty"> |
||||
{{.CsrfTokenHtml}} |
||||
<div class="field"> |
||||
<label for="client-id">{{.locale.Tr "settings.oauth2_client_id"}}</label> |
||||
<input id="client-id" readonly value="{{.App.ClientID}}"> |
||||
</div> |
||||
{{if .ClientSecret}} |
||||
<div class="field"> |
||||
<label for="client-secret">{{.locale.Tr "settings.oauth2_client_secret"}}</label> |
||||
<input id="client-secret" type="text" readonly value="{{.ClientSecret}}"> |
||||
</div> |
||||
{{else}} |
||||
<div class="field"> |
||||
<label for="client-secret">{{.locale.Tr "settings.oauth2_client_secret"}}</label> |
||||
<input id="client-secret" type="password" readonly value="averysecuresecret"> |
||||
</div> |
||||
{{end}} |
||||
<div class="item"> |
||||
<!-- TODO add regenerate secret functionality */ --> |
||||
<form class="ui form ignore-dirty" action="{{.FormActionPath}}/regenerate_secret" method="post"> |
||||
{{.CsrfTokenHtml}} |
||||
{{.locale.Tr "settings.oauth2_regenerate_secret_hint"}} |
||||
<button class="ui tertiary button" type="submit">{{.locale.Tr "settings.oauth2_regenerate_secret"}}</button> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
<div class="ui attached bottom segment"> |
||||
<form class="ui form ignore-dirty" action="{{.FormActionPath}}" method="post"> |
||||
{{.CsrfTokenHtml}} |
||||
<div class="field {{if .Err_AppName}}error{{end}}"> |
||||
<label for="application-name">{{.locale.Tr "settings.oauth2_application_name"}}</label> |
||||
<input id="application-name" value="{{.App.Name}}" name="application_name" required> |
||||
</div> |
||||
<div class="field {{if .Err_RedirectURI}}error{{end}}"> |
||||
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label> |
||||
<input type="url" name="redirect_uri" value="{{.App.PrimaryRedirectURI}}" id="redirect-uri"> |
||||
</div> |
||||
<button class="ui green button"> |
||||
{{.locale.Tr "settings.save_application"}} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
@ -0,0 +1,55 @@ |
||||
<div class="ui attached segment"> |
||||
<div class="ui key list"> |
||||
<div class="item"> |
||||
{{.locale.Tr "settings.oauth2_application_create_description"}} |
||||
</div> |
||||
{{range $app := .Applications}} |
||||
<div class="item"> |
||||
<div class="right floated content"> |
||||
<a href="{{$.Link}}/oauth2/{{$app.ID}}" class="ui primary tiny button"> |
||||
{{svg "octicon-pencil" 16 "mr-2"}} |
||||
{{$.locale.Tr "settings.oauth2_application_edit"}} |
||||
</a> |
||||
<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" |
||||
data-url="{{$.Link}}/oauth2/{{$app.ID}}/delete"> |
||||
{{svg "octicon-trash" 16 "mr-2"}} |
||||
{{$.locale.Tr "settings.delete_key"}} |
||||
</button> |
||||
</div> |
||||
<div class="content"> |
||||
<strong>{{$app.Name}}</strong> |
||||
</div> |
||||
</div> |
||||
{{end}} |
||||
</div> |
||||
</div> |
||||
<div class="ui attached bottom segment"> |
||||
<h5 class="ui top header"> |
||||
{{.locale.Tr "settings.create_oauth2_application"}} |
||||
</h5> |
||||
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post"> |
||||
{{.CsrfTokenHtml}} |
||||
<div class="field {{if .Err_AppName}}error{{end}}"> |
||||
<label for="application-name">{{.locale.Tr "settings.oauth2_application_name"}}</label> |
||||
<input id="application-name" name="application_name" value="{{.application_name}}" required> |
||||
</div> |
||||
<div class="field {{if .Err_RedirectURI}}error{{end}}"> |
||||
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label> |
||||
<input type="url" name="redirect_uri" id="redirect-uri"> |
||||
</div> |
||||
<button class="ui green button"> |
||||
{{.locale.Tr "settings.create_oauth2_application_button"}} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
|
||||
<div class="ui small basic delete modal" id="remove-gitea-oauth2-application"> |
||||
<div class="ui icon header"> |
||||
{{svg "octicon-trash"}} |
||||
{{.locale.Tr "settings.remove_oauth2_application"}} |
||||
</div> |
||||
<div class="content"> |
||||
<p>{{.locale.Tr "settings.oauth2_application_remove_description"}}</p> |
||||
</div> |
||||
{{template "base/delete_modal_actions" .}} |
||||
</div> |
Loading…
Reference in new issue