Files
gitocean/templates/user/settings/authorized_integrations/view_footer.tmpl
T
Mathieu FenniakandMathieu Fenniak cf087a2f12 feat: ability to edit authorized integration in web UI (#12601)
Extends the UI introduced in #12558 to have edit capabilities.  (not in scope: "Add" for a new Authorized Integration will be the next update to this UI; `create-authorized-integration` CLI is still the only way to create a new record)

This PR includes a few refactoring steps.  The goal of these steps is to have `services/auth` be a single entrypoint for validating, inserting, or updating an authorized integration.  Some logic is moved out of `services/authz` because it is not authorization related, and some is moved out of `services/auth/method` to allow it to be reused during validation without creating a cyclical module dependency.

This PR also adds comprehensive validation to the more complex fields in the authorized integration, such as the issuer and claim rules.  This validation applies to the `forgejo admin user create-authorized-integration` CLI as well.

The visible UI is the same as #12558, but with a "Save" button, and the ability to display errors:

![Screenshot 2026-05-16 at 15-43-20 Authorized Integrations - Forgejo Beyond coding. We Forge](/attachments/ffaf60e2-3652-429b-a815-b339100f05f8)

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Tests for JavaScript changes

- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [x] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.
    - Documentation is on my TODO list and will be completed before release.

### Release notes

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12601
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
2026-05-17 18:33:39 +02:00

181 lines
8.6 KiB
Go HTML Template

<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.authorized_integration.perms.title"}}
</h4>
<div class="ui attached bottom segment">
<div class="field">
<fieldset>
<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
<div class="field">
<div class="field ui radio checkbox">
<input id="resource-all" class="enable-system" type="radio" name="resource" value="all" {{if eq .Form.Resource "all"}}checked{{end}}>
<label for="resource-all">{{ctx.Locale.Tr "settings.permissions_access_all"}}</label>
<p class="help">{{ctx.Locale.Tr "settings.access_token.resource_all_help"}}</p>
</div>
</div>
<div class="field">
<div class="field ui radio checkbox">
<input id="resource-public-only" class="enable-system" type="radio" name="resource" value="public-only" {{if eq .Form.Resource "public-only"}}checked{{end}}>
<label for="resource-public-only">{{ctx.Locale.Tr "settings.permissions_public_only"}}</label>
<p class="help">
{{ctx.Locale.Tr "settings.access_token.resource_public_only_help"}}
{{if $.IsAdmin}}{{ctx.Locale.Tr "settings.access_token.admin_disabled"}}{{end}}
</p>
</div>
</div>
<div class="field">
<div class="field ui radio checkbox {{if .Err_SelectedRepo}}error{{end}}">
<input id="resource-repo-specific" class="enable-system" type="radio" name="resource" value="repo-specific" {{if eq .Form.Resource "repo-specific"}}checked{{end}}>
<label for="resource-repo-specific">{{ctx.Locale.Tr "settings.permissions_access_specific_repositories"}}</label>
<p class="help">
{{ctx.Locale.Tr "settings.access_token.resource_specific_repo_help"}}
{{if $.IsAdmin}}{{ctx.Locale.Tr "settings.access_token.admin_disabled"}}{{end}}
</p>
</div>
</div>
</fieldset>
</div>
<div id="repo-selector-wrapper" role="group" class="field tw-mt-4">
<div id="repo-selector" class="tw-flex tw-flex-wrap tw-gap-8">
<!-- left-hand side: repo list from a search -->
<div class="ui tab active list tw-flex-1">
<h5>
{{ctx.Locale.Tr "settings.access_token.available_repositories"}}
</h5>
<div class="ui small fluid action left icon input tw-mb-3">
<input type="search" name="repo_search" spellcheck="false" {{if eq .Autofocus "search"}}autofocus{{end}} placeholder="{{ctx.Locale.Tr "search.repo_kind"}}" value="{{.repo_search}}">
<i class="icon">{{svg "octicon-search" 16}}</i>
<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "search.search"}}" type="submit" name="set_page" value="1" formnovalidate="true" formmethod="get" formaction="#resource-repo-specific">
{{svg "octicon-search" 16}}
</button>
</div>
{{if eq (len .Repos) 0}}
{{ctx.Locale.Tr "settings.access_token.no_repositories_found"}}
{{else}}
<div class="tw-grid tw-items-center" style="grid-template-columns: min-content 1fr min-content;">
{{range .Repos}}
{{template "user/settings/repo_icon" .}}
<div class="text truncate">
{{.FullName}}
</div>
<button class="ui primary button tw-ml-2 tw-my-1 tiny" type="submit" aria-label="{{ctx.Locale.Tr "repo.editor.add" .FullName}}" formnovalidate="true" name="add_selected_repo" value="{{.FullName}}" formmethod="get" formaction="#resource-repo-specific">
{{ctx.Locale.Tr "add"}}
</button>
{{end}}
</div>
{{end}}
{{/* Can't use base/paginate template include here because all the pagination links in
that template are simple <a href=...> links. Here, we need to turn them into form
buttons so that we can submit the current form. If a user just changed a value (eg. set
the token name, changed a selected permission) and then clicked a pagination button, the
new value that they changed needs to be submitted. base/paginate would allow preserving
old values from before the change, but not new updates. Implementing here also allows
the use of smaller styling. */}}
{{with .Page.Paginater}}
<input type="hidden" name="page" value="{{.Current}}">
<div class="center page buttons">
<div class="ui borderless pagination menu mini">
<button class="item navigation {{if .IsFirst}}disabled{{end}}" type="submit" formnovalidate="true" name="set_page" value="1" formmethod="get" formaction="#resource-repo-specific">
{{svg "gitea-double-chevron-left" 16 "tw-mr-1"}}
<span class="navigation_label">{{ctx.Locale.Tr "admin.first_page"}}</span>
</button>
<button class="item navigation {{if not .HasPrevious}}disabled{{end}}" type="submit" formnovalidate="true" name="set_page" value="{{.Previous}}" formmethod="get" formaction="#resource-repo-specific">
{{svg "octicon-chevron-left" 16 "tw-mr-1"}}
<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.previous"}}</span>
</button>
{{range .Pages}}
{{if eq .Num -1}}
<a class="disabled item">...</a>
{{else}}
<button class="item navigation {{if .IsCurrent}}active{{end}}" type="submit" formnovalidate="true" name="set_page" value="{{.Num}}" formmethod="get" formaction="#resource-repo-specific">
{{.Num}}
</button>
{{end}}
{{end}}
<button class="item navigation {{if not .HasNext}}disabled{{end}}" type="submit" formnovalidate="true" name="set_page" value="{{.Next}}" formmethod="get" formaction="#resource-repo-specific">
<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.next"}}</span>
{{svg "octicon-chevron-right" 16 "tw-ml-1"}}
</button>
<button class="item navigation {{if .IsLast}}disabled{{end}}" type="submit" formnovalidate="true" name="set_page" value="{{.TotalPages}}" formmethod="get" formaction="#resource-repo-specific">
<span class="navigation_label">{{ctx.Locale.Tr "admin.last_page"}}</span>
{{svg "gitea-double-chevron-right" 16 "tw-ml-1"}}
</button>
</div>
</div>
{{end}}
</div>
<!-- right-hand side: selected repositories -->
<div class="tw-flex-1">
<h5>{{ctx.Locale.TrPluralString (len .SelectedRepos) "settings.access_token.selected_repositories" (len .SelectedRepos)}}</h5>
{{if eq (len .SelectedRepos) 0}}
<div class="tw-my-2">
{{ctx.Locale.Tr "settings.access_token.no_repositories_selected"}}
</div>
{{else}}
<div class="tw-grid tw-items-center tw-auto-rows-min" style="grid-template-columns: min-content 1fr min-content;">
{{range .SelectedRepos}}
<input type="hidden" name="selected_repo" value="{{.FullName}}">
{{template "user/settings/repo_icon" .}}
<div class="text tw-text-nowrap"> <!-- nowrap, !truncate, ensures that this can be fully seen and ensures this side of the control has an appropriate min-width -->
{{.FullName}}
</div>
<button class="ui button tw-ml-2 tw-my-1 tiny" type="submit" aria-label="{{ctx.Locale.Tr "settings.access_token.remove" .FullName}}" formnovalidate="true" name="remove_selected_repo" value="{{.FullName}}" formmethod="get" formaction="#resource-repo-specific">
{{ctx.Locale.Tr "remove"}}
</button>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
<div class="field {{if .Err_Scope}}error{{end}}">
<label>
{{ctx.Locale.Tr "settings.select_permissions"}}
</label>
<p class="activity meta">
<p>{{ctx.Locale.Tr "settings.access_token_desc" (printf "%s/api/swagger" AppSubUrl) "https://forgejo.org/docs/latest/user/token-scope/"}}</p>
</p>
{{range .Categories}}
<div class="field tw-pl-1 tw-pb-1">
<label for="scope-{{.}}">
{{.}}
</label>
<div class="gitea-select">
<select class="ui selection" name="scope" id="scope-{{.}}">
<option value="">
{{ctx.Locale.Tr "settings.permission_no_access"}}
</option>
<option value="read:{{.}}" {{if (SliceUtils.Contains $.Form.Scope (printf "read:%s" .))}} selected {{end}}>
{{ctx.Locale.Tr "settings.permission_read"}}
</option>
<option value="write:{{.}}" {{if or $.Form.ScopeAll (SliceUtils.Contains $.Form.Scope (printf "write:%s" .))}} selected {{end}}>
{{ctx.Locale.Tr "settings.permission_write"}}
</option>
</select>
</div>
</div>
{{end}}
</div>
</div>
<div class="ui segment">
<button class="ui primary button">{{ctx.Locale.Tr "settings.authorized_integration.save"}}</button>
</div>
</form>
</div>
{{template "user/settings/layout_footer" .}}