Implement consent management
The primary change in this commit is the introduction of consent management. A few minor improvements have been made: - move common header to ui/templates/base.gohtml - add an I18NService to unify localization - add a handlers.getLocalizer function - fix translation extraction and merging in Makefile - add a new AuthMiddleware to centralize client certificate authentication - move client certificate handling to internal/handlers/security.go - improver error handling, allow localization of HTTP error messagesmain
parent
679dcb27ce
commit
44e18ca3a5
@ -1,10 +1,11 @@
|
|||||||
*.pem
|
*.pem
|
||||||
.idea/
|
/.idea/
|
||||||
/cacert-idp
|
/cacert-idp
|
||||||
|
/certs/
|
||||||
/dist/
|
/dist/
|
||||||
/idp.toml
|
/idp.toml
|
||||||
/static
|
/static
|
||||||
|
/translations/translate.*.toml
|
||||||
/ui/css/
|
/ui/css/
|
||||||
/ui/images/
|
/ui/images/
|
||||||
/ui/js/
|
/ui/js/
|
||||||
certs/
|
|
@ -0,0 +1,370 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 CAcert Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
client "github.com/ory/hydra-client-go/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"code.cacert.org/cacert/oidc-idp/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexHandler struct {
|
||||||
|
logger *log.Logger
|
||||||
|
trans *services.I18NService
|
||||||
|
templates TemplateCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localizer := getLocalizer(h.trans, r)
|
||||||
|
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.Error(w, h.trans.LookupHTTPErrorMessage(http.StatusNotFound, localizer), http.StatusNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.templates.render(h.logger, w, Index, map[string]interface{}{
|
||||||
|
"Title": h.trans.LookupMessage("IndexTitle", nil, localizer),
|
||||||
|
"WelcomeMessage": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
|
||||||
|
"IndexWelcomeMessage",
|
||||||
|
map[string]interface{}{"ManageConsentHRef": "/manage-consent"},
|
||||||
|
localizer,
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndex(logger *log.Logger, templateCache TemplateCache, trans *services.I18NService) *IndexHandler {
|
||||||
|
return &IndexHandler{logger: logger, trans: trans, templates: templateCache}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManageConsentHandler struct {
|
||||||
|
logger *log.Logger
|
||||||
|
trans *services.I18NService
|
||||||
|
adminAPI client.OAuth2Api
|
||||||
|
templates TemplateCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ManageConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localizer := getLocalizer(h.trans, r)
|
||||||
|
|
||||||
|
allSessions := make(ConsentSessions, 0)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sessions []client.OAuth2ConsentSession
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, s := range GetAuthenticatedAddresses(r) {
|
||||||
|
if sessions, ok = h.getConsentSessions(w, r, s); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, session := range sessions {
|
||||||
|
allSessions = append(allSessions, ConsentSession{Subject: s, Session: session})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(allSessions)
|
||||||
|
|
||||||
|
requestSession, err := GetSession(r)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.WithError(err).Error("could not get session for request")
|
||||||
|
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.templates.render(h.logger, w, ManageConsent, map[string]interface{}{
|
||||||
|
"Title": h.trans.LookupMessage("ManageConsentTitle", nil, localizer),
|
||||||
|
"Description": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
|
||||||
|
"ManageConsentDescription",
|
||||||
|
nil,
|
||||||
|
localizer,
|
||||||
|
)),
|
||||||
|
"NoConsentGiven": h.trans.LookupMessage("NoConsentGiven", nil, localizer),
|
||||||
|
"Flashes": requestSession.Flashes("messages"),
|
||||||
|
"ConsentSessions": allSessions,
|
||||||
|
"ButtonTitleRevoke": h.trans.LookupMessage("ButtonTitleRevoke", nil, localizer),
|
||||||
|
"ApplicationTitle": h.trans.LookupMessage("ColumnNameApplication", nil, localizer),
|
||||||
|
"ActionsTitle": h.trans.LookupMessage("ColumnNameActions", nil, localizer),
|
||||||
|
"SubjectTitle": h.trans.LookupMessage("ColumnNameSubject", nil, localizer),
|
||||||
|
"GrantedTitle": h.trans.LookupMessage("ColumnNameGranted", nil, localizer),
|
||||||
|
"ExpiresTitle": h.trans.LookupMessage("ColumnNameExpires", nil, localizer),
|
||||||
|
"LabelUnknown": h.trans.LookupMessage("LabelUnknown", nil, localizer),
|
||||||
|
"LabelNever": h.trans.LookupMessage("LabelNever", nil, localizer),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConsentSession struct {
|
||||||
|
Subject string
|
||||||
|
Session client.OAuth2ConsentSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ConsentSession) GetClientName() string {
|
||||||
|
if !s.Session.HasConsentRequest() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
request := s.Session.GetConsentRequest()
|
||||||
|
if !request.HasClient() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
consentClient := request.GetClient()
|
||||||
|
|
||||||
|
return consentClient.GetClientName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ConsentSession) GetID() string {
|
||||||
|
if consent, ok := s.Session.GetConsentRequestOk(); ok {
|
||||||
|
if app, ok := consent.GetClientOk(); ok {
|
||||||
|
return app.GetClientId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ConsentSession) GrantedAt() *time.Time {
|
||||||
|
if grantedAt, ok := s.Session.GetHandledAtOk(); ok {
|
||||||
|
return grantedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ConsentSession) Expires() *time.Time {
|
||||||
|
if expiresAt, ok := s.Session.GetExpiresAtOk(); ok {
|
||||||
|
if grantedAt := s.GrantedAt(); grantedAt != nil {
|
||||||
|
return expiresAt.AccessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConsentSessions []ConsentSession
|
||||||
|
|
||||||
|
func (c ConsentSessions) Len() int {
|
||||||
|
return len(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConsentSessions) Less(i, j int) bool {
|
||||||
|
return c[i].Subject < c[j].Subject && c[i].GetClientName() < c[j].GetClientName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ConsentSessions) Swap(i, j int) {
|
||||||
|
c[i], c[j] = c[j], c[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ManageConsentHandler) getConsentSessions(
|
||||||
|
w http.ResponseWriter, r *http.Request, subject string,
|
||||||
|
) ([]client.OAuth2ConsentSession, bool) {
|
||||||
|
sessions, response, err := h.adminAPI.ListOAuth2ConsentSessions(r.Context()).Subject(subject).Execute()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.WithError(err).Error("error getting consent session list")
|
||||||
|
|
||||||
|
// h.fillAcceptLoginRequestErrorBucket(r, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(response *http.Response) { _ = response.Body.Close() }(response)
|
||||||
|
|
||||||
|
h.logger.WithFields(
|
||||||
|
log.Fields{"response": response.Status, "consent_sessions": sessions},
|
||||||
|
).Debug("got response for AcceptOAuth2LoginRequest")
|
||||||
|
|
||||||
|
if h.logger.IsLevelEnabled(log.TraceLevel) {
|
||||||
|
if rb, err := io.ReadAll(response.Body); err == nil {
|
||||||
|
h.logger.WithField("response_body", rb).Trace("response body from Hydra")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManageConsent(
|
||||||
|
logger *log.Logger, tc TemplateCache, trans *services.I18NService, adminAPI client.OAuth2Api,
|
||||||
|
) *ManageConsentHandler {
|
||||||
|
return &ManageConsentHandler{logger: logger, trans: trans, adminAPI: adminAPI, templates: tc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevokeConsentHandler struct {
|
||||||
|
logger *log.Logger
|
||||||
|
trans *services.I18NService
|
||||||
|
adminAPI client.OAuth2Api
|
||||||
|
templates TemplateCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RevokeConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
localizer := getLocalizer(h.trans, r)
|
||||||
|
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||||
|
http.Error(
|
||||||
|
w, h.trans.LookupHTTPErrorMessage(http.StatusMethodNotAllowed, localizer), http.StatusMethodNotAllowed,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID, _ := strings.CutPrefix(r.URL.Path, "/revoke-consent/")
|
||||||
|
subject := r.URL.Query().Get("subject")
|
||||||
|
|
||||||
|
if clientID == "" || subject == "" {
|
||||||
|
http.Error(w, h.trans.LookupHTTPErrorMessage(http.StatusNotFound, localizer), http.StatusNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
h.handleGet(w, r, clientID, localizer, subject)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.handlePost(w, r, clientID, subject, localizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RevokeConsentHandler) handleGet(
|
||||||
|
w http.ResponseWriter, r *http.Request, clientID string, localizer *i18n.Localizer, subject string,
|
||||||
|
) {
|
||||||
|
clientApp, found, err := h.getClient(r.Context(), clientID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.WithError(err).Error("could not get client")
|
||||||
|
|
||||||
|
http.Error(
|
||||||
|
w, h.trans.LookupHTTPErrorMessage(http.StatusInternalServerError, localizer),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
http.Error(w, h.trans.LookupHTTPErrorMessage(http.StatusNotFound, localizer), http.StatusNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.templates.render(h.logger, w, ConfirmRevoke, map[string]interface{}{
|
||||||
|
"Title": h.trans.LookupMessage("ConfirmRevokeTitle", nil, localizer),
|
||||||
|
"ButtonTitleRevoke": h.trans.LookupMessage("ButtonTitleConfirmRevoke", nil, localizer),
|
||||||
|
"ButtonTitleCancel": h.trans.LookupMessage("ButtonTitleCancel", nil, localizer),
|
||||||
|
"CancelLink": "/manage-consent",
|
||||||
|
csrf.TemplateTag: csrf.TemplateField(r),
|
||||||
|
"Explanation": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
|
||||||
|
"ConfirmRevokeExplanation", map[string]interface{}{
|
||||||
|
"ApplicationID": template.URLQueryEscaper(clientApp.GetClientId()),
|
||||||
|
"Application": template.HTMLEscapeString(clientApp.GetClientName()),
|
||||||
|
"Subject": subject,
|
||||||
|
}, localizer)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RevokeConsentHandler) handlePost(
|
||||||
|
w http.ResponseWriter, r *http.Request, clientID string, subject string, localizer *i18n.Localizer,
|
||||||
|
) {
|
||||||
|
err := h.revokeConsent(r.Context(), clientID, subject)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.WithError(err).Error("could not revoke consent")
|
||||||
|
|
||||||
|
http.Error(
|
||||||
|
w, h.trans.LookupHTTPErrorMessage(http.StatusInternalServerError, localizer),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/manage-consent", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RevokeConsentHandler) getClient(ctx context.Context, clientID string) (*client.OAuth2Client, bool, error) {
|
||||||
|
clientApp, response, err := h.adminAPI.GetOAuth2Client(ctx, clientID).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("error getting client application: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(response *http.Response) { _ = response.Body.Close() }(response)
|
||||||
|
|
||||||
|
h.logger.WithFields(
|
||||||
|
log.Fields{"response": response.Status, "client_app": clientApp},
|
||||||
|
).Debug("got response for GetOAuth2Client")
|
||||||
|
|
||||||
|
if h.logger.IsLevelEnabled(log.TraceLevel) {
|
||||||
|
if rb, err := io.ReadAll(response.Body); err == nil {
|
||||||
|
h.logger.WithField("response_body", rb).Trace("response body from Hydra")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientApp, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RevokeConsentHandler) revokeConsent(ctx context.Context, clientID, subject string) error {
|
||||||
|
response, err := h.adminAPI.RevokeOAuth2ConsentSessions(ctx).Client(clientID).Subject(subject).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not revoke consent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(response *http.Response) { _ = response.Body.Close() }(response)
|
||||||
|
|
||||||
|
h.logger.WithFields(
|
||||||
|
log.Fields{"response": response.Status},
|
||||||
|
).Debug("got response for RevokeOAuth2ConsentSessions")
|
||||||
|
|
||||||
|
if h.logger.IsLevelEnabled(log.TraceLevel) {
|
||||||
|
if rb, err := io.ReadAll(response.Body); err == nil {
|
||||||
|
h.logger.WithField("response_body", rb).Trace("response body from Hydra")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRevokeConsent(
|
||||||
|
logger *log.Logger, tc TemplateCache, trans *services.I18NService, adminAPI client.OAuth2Api,
|
||||||
|
) *RevokeConsentHandler {
|
||||||
|
return &RevokeConsentHandler{logger: logger, trans: trans, adminAPI: adminAPI, templates: tc}
|
||||||
|
}
|
@ -1,36 +1,37 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form class="form-signin" method="post">
|
<main role="main" class="container">
|
||||||
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
<h1>{{ .Title }}</h1>
|
||||||
<h1 class="h3 mb-3">{{ .Title }}</h1>
|
<p>{{ .IntroText }}</p>
|
||||||
<p class="text-left">{{ .IntroText }}</p>
|
|
||||||
<p class="text-left">{{ .EmailChoiceText }}</p>
|
|
||||||
{{ with .FlashMessage }}
|
{{ with .FlashMessage }}
|
||||||
<div class="alert alert-{{ .Type }}" role="alert">
|
<div class="alert alert-{{ .Type }}" role="alert">
|
||||||
{{ .Message }}
|
{{ .Message }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="mb-3">
|
<form method="post">
|
||||||
{{ if eq (len .emails) 1 }}
|
<p>{{ .EmailChoiceText }}</p>
|
||||||
{{ $email_address := index .emails 0 }}
|
<div class="mb-3">
|
||||||
<input type="hidden" name="email" value="{{ $email_address }}" id="email_0">
|
{{ if eq (len .emails) 1 }}
|
||||||
<label for="email_0">{{ $email_address }}</label>
|
{{ $email_address := index .emails 0 }}
|
||||||
{{ else }}
|
<input type="hidden" name="email" value="{{ $email_address }}" id="email_0">
|
||||||
{{ range $index, $element := .emails }}
|
<label for="email_0">{{ $email_address }}</label>
|
||||||
<div class="form-check">
|
{{ else }}
|
||||||
<input class="form-check-input" type="radio" name="email"
|
{{ range $index, $element := .emails }}
|
||||||
value="{{ $element }}" id="email_{{ $index }}"><label
|
<div class="form-check">
|
||||||
class="form-check-label" for="email_{{ $index }}">{{ $element }}</label>
|
<input class="form-check-input" type="radio" name="email"
|
||||||
</div>
|
value="{{ $element }}" id="email_{{ $index }}"><label
|
||||||
|
class="form-check-label" for="email_{{ $index }}">{{ $element }}</label>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ .csrfField }}
|
||||||
{{ .csrfField }}
|
</div>
|
||||||
</div>
|
<p class="text-left">{{ .RequestText }}</p>
|
||||||
<p class="text-left">{{ .RequestText }}</p>
|
<div class="mb-2">
|
||||||
<div class="mb-2">
|
<button class="btn btn-primary" type="submit" name="use-identity"
|
||||||
<button class="btn btn-primary" type="submit" name="use-identity"
|
value="accept">{{ .AcceptLabel }}</button>
|
||||||
value="accept">{{ .AcceptLabel }}</button>
|
<button class="btn btn-outline-secondary" type="submit" name="use-identity"
|
||||||
<button class="btn btn-outline-secondary" type="submit" name="use-identity"
|
value="reject">{{ .RejectLabel }}</button>
|
||||||
value="reject">{{ .RejectLabel }}</button>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</main>
|
||||||
{{ end }}
|
{{ end }}
|
@ -0,0 +1,11 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<main role="main" class="container">
|
||||||
|
<h1 class="h3 mb-3">{{ .Title }}</h1>
|
||||||
|
<p class="text-left">{{ .Explanation }}</p>
|
||||||
|
<form method="post">
|
||||||
|
{{ .csrfField }}
|
||||||
|
<button class="btn btn-danger" type="submit">{{ .ButtonTitleRevoke }}</button>
|
||||||
|
<a class="btn btn-outline-secondary" role="button" href="{{ .CancelLink }}">{{ .ButtonTitleCancel }}</a>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
@ -1,13 +1,15 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="container">
|
<main role="main" class="container">
|
||||||
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
|
||||||
<h1>{{ .Title }}</h1>
|
<h1>{{ .Title }}</h1>
|
||||||
<h2>{{ if .details.ErrorCode }}
|
<div class="alert alert-danger">
|
||||||
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2>
|
<h2 class="alert-heading">{{ if .details.ErrorCode }}
|
||||||
{{ if .details.ErrorDetails }}
|
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}
|
||||||
{{ range .details.ErrorDetails }}
|
</h2>
|
||||||
<p>{{ . }}</p>
|
{{ if .details.ErrorDetails }}
|
||||||
|
{{ range .details.ErrorDetails }}
|
||||||
|
{{ . }}
|
||||||
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
</div>
|
||||||
</div>
|
</main>
|
||||||
{{ end }}
|
{{ end }}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<main role="main" class="container">
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<p>{{ .WelcomeMessage }}</p>
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
@ -0,0 +1,47 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<main role="main" class="container">
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<p class="text-left">{{ .Description }}</p>
|
||||||
|
{{ range .Flashes}}
|
||||||
|
<div class="alert alert-{{ .Type }}" role="alert">
|
||||||
|
{{ .Message }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ $buttonTitleRevoke := .ButtonTitleRevoke }}
|
||||||
|
{{ $unknownLabel := .LabelUnknown }}
|
||||||
|
{{ $neverLabel := .LabelNever }}
|
||||||
|
{{ if .ConsentSessions }}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ .ApplicationTitle }}</th>
|
||||||
|
<th>{{ .SubjectTitle }}</th>
|
||||||
|
<th>{{ .GrantedTitle }}</th>
|
||||||
|
<th>{{ .ExpiresTitle }}</th>
|
||||||
|
<th>{{ .ActionsTitle }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range .ConsentSessions }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .GetClientName }}</td>
|
||||||
|
<td>{{ .Subject }}</td>
|
||||||
|
<td>{{ if .GrantedAt }}<span
|
||||||
|
title="{{ .GrantedAt }}">{{ .GrantedAt | humantime }}</span>{{ else }}{{ $unknownLabel }}{{ end }}
|
||||||
|
</td>
|
||||||
|
<td>{{ if .Expires }}<span
|
||||||
|
title="{{ .Expires }}">{{ .Expires | humantime }}</span>{{ else }}{{ $neverLabel }}{{ end }}
|
||||||
|
</td>
|
||||||
|
<td><a href="/revoke-consent/{{ .GetID }}?subject={{ .Subject }}" class="btn btn-danger"
|
||||||
|
role="button">{{ $buttonTitleRevoke }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ else}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{ .NoConsentGiven }}
|
||||||
|
</div>
|
||||||
|
{{ end}}
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
@ -1,7 +1,6 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="container">
|
<main role="main" class="container">
|
||||||
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
<h1>{{ .Title }}</h1>
|
||||||
<h1 class="h3 mb-3">{{ .Title }}</h1>
|
<p>{{ .Explanation }}</p>
|
||||||
<p class="text-left">{{ .Explanation }}</p>
|
</main>
|
||||||
</div>
|
|
||||||
{{ end }}
|
{{ end }}
|
@ -1,7 +1,6 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="container">
|
<main role="main" class="container">
|
||||||
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
<h1>{{ .Title }}</h1>
|
||||||
<h1 class="h3 mb-3">{{ .Title }}</h1>
|
<p>{{ .Explanation }}</p>
|
||||||
<p class="text-left">{{ .Explanation }}</p>
|
</main>
|
||||||
</div>
|
|
||||||
{{ end }}
|
{{ end }}
|
Loading…
Reference in New Issue