Skip to content

Commit

Permalink
feat(APIKeys: (67) Added API Key UI (#69)
Browse files Browse the repository at this point in the history
* update to api-key-ui

* update

* update

* Added API Key ui

* remove

* update to api key create page

* update to api key create page

* added permission select ui

* remove logs

* some review comments

* processed review comments

* work on merging ui services

* removed deps

* update api key ui to be in tenants settings page

* makefile admin cmd to create base user, added snackbar to tenants ui

* added ordered map key for showing permissions in create api key page

* Partial changes

* added flash messages

* update flash messages

* update flash messages utils

* some changes

* update to api key ui and some changes in api err handling in dashboard

* moved flash messages to pkg

* added close button to flash message

* removed test err

* removed logs

* added comments

---------

Co-authored-by: Tim van Osch <[email protected]>
  • Loading branch information
JEFFTheDev and TimVosch authored Jan 23, 2024
1 parent 6dd87ec commit 8e03045
Show file tree
Hide file tree
Showing 68 changed files with 5,956 additions and 1,141 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ golib: golib-clean
--git-host=sensorbucket.nl --git-repo-id=api \
--enable-post-process-file \
--additional-properties=packageName=api,packageUrl='https://sensorbucket.nl'

admin:
echo '{"schema_id":"default", "traits": {"email":"[email protected]"}}' | http post 127.0.0.1:4434/admin/identities | jq .id | \
xargs -I uid echo '{"identity_id":"uid"}' | http post 127.0.0.1:4434/admin/recovery/code
6 changes: 4 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ services:
- ./tools/docker-compose/oathkeeper_config:/etc/config/oathkeeper
environment:
- LOG_LEAK_SENSITIVE_VALUES=true
# Use own build of ory kratos since latest ory kratos does not contain latest changes which we need
# for certain Kratos features
kratos-migrate:
image: oryd/kratos:latest
image: ghcr.io/sensorbucket/oryd/kratos:latest
environment:
- DSN=postgres://sensorbucket:sensorbucket@db:5432/kratos?sslmode=disable
volumes:
Expand All @@ -136,7 +138,7 @@ services:
kratos:
depends_on:
- kratos-migrate
image: oryd/kratos:latest
image: ghcr.io/sensorbucket/oryd/kratos:latest
ports:
- '4433:4433' # public
- '4434:4434' # admin
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/google/uuid v1.3.0
github.com/gorilla/schema v1.2.0
github.com/gorilla/websocket v1.5.0
github.com/jackc/pgx/v5 v5.3.1
github.com/jackc/pgx/v5 v5.5.2
github.com/jmoiron/sqlx v1.3.5
github.com/ory/client-go v1.5.1
github.com/prometheus/client_golang v1.15.1
Expand Down Expand Up @@ -78,6 +78,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.0 // indirect
Expand Down Expand Up @@ -120,6 +121,7 @@ require (
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA=
github.com/jackc/pgx/v5 v5.5.2/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
Expand Down
181 changes: 181 additions & 0 deletions internal/flash_messages/flash-messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package flash_messages

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
)

type FlashMessage struct {
Title string
Description string
MessageType FlashMessageType
CopyButton bool
}

type FlashMessageType int

const (
Warning FlashMessageType = iota
Error
Success
)

type FlashMessageRenderer func(msg FlashMessage) string

type FlashMessages []FlashMessage

type ctxKey int

const (
ctxFlashMessagesKey ctxKey = iota
)

func (fm *FlashMessages) AsCookie() (http.Cookie, error) {
b, err := json.Marshal(&fm)
if err != nil {
return http.Cookie{}, err
}

return http.Cookie{
Name: "flash_messages",
Value: base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(b),
Secure: true,
HttpOnly: true,
Path: "/",

// If somehow the messages are not shown after 5 seconds they're not relevant anymore
MaxAge: 5,
}, nil
}

func ExtractFlashMessage(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if cookie, err := r.Cookie("flash_messages"); err == nil {

// found flash messages to display to the user
flashMessages := FlashMessages{}
decoded, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(cookie.Value)
if err != nil {
fmt.Printf("[Warning] found flash_messages cookie, but was not a valid base64 string\n")
} else {
err = json.Unmarshal(decoded, &flashMessages)
if err != nil {
fmt.Printf("[Warning] found flash_messages cookie, but couldnt convert to flash message: %s\n", err)
} else {
ctx = context.WithValue(ctx, ctxFlashMessagesKey, flashMessages)
}
}

// unset the cookie, flash messages should only be shown once
cookie.MaxAge = -1
cookie.Path = "/"
http.SetCookie(w, cookie)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func FlashMessagesFromContext(ctx context.Context) (FlashMessages, bool) {
if messages := ctx.Value(ctxFlashMessagesKey); messages != nil {
if flashMessages, ok := messages.(FlashMessages); ok {
return flashMessages, ok
} else {
log.Printf("[Warning] found flash messages in request context but couldnt parse\n")
}
}
return FlashMessages{}, false
}

func AddContextFlashMessages(r *http.Request, container *FlashMessagesContainer) {
if inContext, ok := FlashMessagesFromContext(r.Context()); ok {
container.contextFlashMessages = inContext
}
container.flashMessages = append(container.stagedFlashMessages, container.contextFlashMessages...)
}

func AddSuccessFlashMessageToPage(r *http.Request, container *FlashMessagesContainer, message string) {
if inContext, ok := FlashMessagesFromContext(r.Context()); ok {
container.contextFlashMessages = inContext
}
container.stagedFlashMessages = append(container.stagedFlashMessages, getFlashMessage("Success", message, Success, false))
container.flashMessages = append(container.contextFlashMessages, container.stagedFlashMessages...)
}

func AddWarningFlashMessageToPage(r *http.Request, container *FlashMessagesContainer, title string, message string, copyButton bool) {
if inContext, ok := FlashMessagesFromContext(r.Context()); ok {
container.contextFlashMessages = inContext
}
container.stagedFlashMessages = append(container.stagedFlashMessages, getFlashMessage(title, message, Warning, copyButton))
container.flashMessages = append(container.contextFlashMessages, container.stagedFlashMessages...)
}

func AddErrorFlashMessageToPage(r *http.Request, container *FlashMessagesContainer, message string) {
if inContext, ok := FlashMessagesFromContext(r.Context()); ok {
container.contextFlashMessages = inContext
}
container.stagedFlashMessages = append(container.stagedFlashMessages, getFlashMessage("Error", message, Error, false))
container.flashMessages = append(container.contextFlashMessages, container.stagedFlashMessages...)
}

func AddSuccessFlashMessage(w http.ResponseWriter, r *http.Request, message string) context.Context {
return addFlashMessage(w, r, "Success", message, Success, false)
}

func AddWarningFlashMessage(w http.ResponseWriter, r *http.Request, title string, message string, copyButton bool) context.Context {
return addFlashMessage(w, r, title, message, Warning, copyButton)
}

func AddErrorFlashMessage(w http.ResponseWriter, r *http.Request, message string) context.Context {
return addFlashMessage(w, r, "Error", message, Error, false)
}

func WriteSuccessFlashMessage(w http.ResponseWriter, r *http.Request, message string) {
fm := getFlashMessage("Success", message, Success, false)
w.Write([]byte(RenderFlashMessage(fm)))
}

func WriteWarningFlashMessage(w http.ResponseWriter, r *http.Request, title string, message string, copyButton bool) {
fm := getFlashMessage("Warning", message, Warning, copyButton)
w.Write([]byte(RenderFlashMessage(fm)))
}

func WriteErrorFlashMessage(w http.ResponseWriter, r *http.Request, message string) {
fm := getFlashMessage("Error", message, Error, false)
w.Write([]byte(RenderFlashMessage(fm)))
}

func addFlashMessage(w http.ResponseWriter, r *http.Request, title string, description string, msgType FlashMessageType, copyButton bool) context.Context {
flashMessages := FlashMessages{}
flashMessages, _ = FlashMessagesFromContext(r.Context())
flashMessages = append(flashMessages, getFlashMessage(title, description, msgType, copyButton))
newCtx := context.WithValue(r.Context(), ctxFlashMessagesKey, flashMessages)
writeFlashMessagesToCookie(w, r.WithContext(newCtx))
return newCtx
}

func getFlashMessage(title string, description string, msgType FlashMessageType, copyButton bool) FlashMessage {
return FlashMessage{
Title: title,
Description: description,
MessageType: msgType,
CopyButton: copyButton,
}
}

func writeFlashMessagesToCookie(w http.ResponseWriter, r *http.Request) {
if messages, ok := FlashMessagesFromContext(r.Context()); ok {
res, err := messages.AsCookie()
if err != nil {
log.Printf("[Warning] couldnt set flash_messages cookie %s\n", err)
} else {
http.SetCookie(w, &res)
}
} else {
log.Printf("[Warning] no flash_messages could be found to be set as cookie\n")
}
}
88 changes: 88 additions & 0 deletions internal/flash_messages/flashMessages.qtpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@

{% func (fm *FlashMessagesContainer) Render() %}
<div id="flash-messages" >
{% for _, flashMessage := range fm.flashMessages %}
{%= RenderFlashMessage(flashMessage) %}
{% endfor %}
</div>
{% endfunc %}

{% func RenderFlashMessage(msg FlashMessage) %}
{% code
color := ""
icon := ""

switch msg.MessageType {
case Success:
color = "green"
icon = "mdi:success-circle-outline"
case Warning:
color = "orange"
icon = "carbon:warning"
case Error:
color = "red"
icon = "icon-park-outline:error"
}
%}
<div class="p-3">
<!-- Required to include below tailwind classes in styles -->
<div class="hidden">
<div class="text-red-600 bg-red-400 border-red-500"></div>
<div class="border-red-400 bg-red-500"></div>
<div class="bg-red-100"></div>

<div class="text-orange-600 bg-orange-400 border-orange-500"></div>
<div class="border-orange-400 bg-orange-500"></div>
<div class="bg-orange-100"></div>

<div class="text-green-600 bg-green-400 border-green-500"></div>
<div class="border-green-400 bg-green-500"></div>
<div class="bg-green-100"></div>
</div>
<div class="bg-{%s color %}-100 border-l-4 flash-message border-{%s color %}-400 text-{%s color %}-600 p-4" role="alert">
<div class="flex w-full">
<div class="py-2">
<iconify-icon icon="{%s icon %}" width="24"
class="px-4 float-right text-{%s color %}-600"></iconify-icon>
</div>
<div class="w-full">
<p class="text-sm font-bold">{%s msg.Title %}</p>
<br />
<div class="flex justify-start">
<p id="dialogue-value" class="text-sm w-full truncate">
{%s msg.Description %}
</p>
{% if msg.CopyButton %}
<button onclick="copyValueToClipboard()"
class="text-sm ml-1 bg-{%s color %}-400 hover:bg-{%s color %}-500 text-white border border-{%s color %}-500 rounded px-2 py-1">
Copy
</button>
{% endif %}
</div>
</div>
<iconify-icon _="on click hide closest .flash-message" class="cursor-pointer" icon="material-symbols-light:close" width="18"></iconify-icon>
</div>
</div>
<script type="text/javascript">
function copyValueToClipboard() {
const text = document.getElementById("dialogue-value").innerText;
navigator.clipboard.writeText(text);
}
</script>

</div>
{% endfunc %}

{% code
type FlashMessagesContainer struct {

// Staged flash messages are set by the package user and will immediately be shown upon rendering
stagedFlashMessages FlashMessages

// Context flash messages are present in the request context, which is based of a cookie value 'flash_messages'
contextFlashMessages FlashMessages

// Accumulation of both staged and context flash messages
flashMessages FlashMessages
}
%}
Loading

0 comments on commit 8e03045

Please sign in to comment.