mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat: add ability to change passwords (resolve #30)
This commit is contained in:
parent
a7c83252ef
commit
0294425de0
21
main.go
21
main.go
@ -85,11 +85,13 @@ func main() {
|
|||||||
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService)
|
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService)
|
||||||
summaryHandler := routes.NewSummaryHandler(summaryService)
|
summaryHandler := routes.NewSummaryHandler(summaryService)
|
||||||
healthHandler := routes.NewHealthHandler(db)
|
healthHandler := routes.NewHealthHandler(db)
|
||||||
|
settingsHandler := routes.NewSettingsHandler(userService)
|
||||||
publicHandler := routes.NewIndexHandler(userService, keyValueService)
|
publicHandler := routes.NewIndexHandler(userService, keyValueService)
|
||||||
|
|
||||||
// Setup Routers
|
// Setup Routers
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
publicRouter := router.PathPrefix("/").Subrouter()
|
publicRouter := router.PathPrefix("/").Subrouter()
|
||||||
|
settingsRouter := publicRouter.PathPrefix("/settings").Subrouter()
|
||||||
summaryRouter := publicRouter.PathPrefix("/summary").Subrouter()
|
summaryRouter := publicRouter.PathPrefix("/summary").Subrouter()
|
||||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||||
|
|
||||||
@ -105,17 +107,24 @@ func main() {
|
|||||||
// Router configs
|
// Router configs
|
||||||
router.Use(loggingMiddleware, recoveryMiddleware)
|
router.Use(loggingMiddleware, recoveryMiddleware)
|
||||||
summaryRouter.Use(authenticateMiddleware)
|
summaryRouter.Use(authenticateMiddleware)
|
||||||
|
settingsRouter.Use(authenticateMiddleware)
|
||||||
apiRouter.Use(corsMiddleware, authenticateMiddleware)
|
apiRouter.Use(corsMiddleware, authenticateMiddleware)
|
||||||
|
|
||||||
// Public Routes
|
// Public Routes
|
||||||
publicRouter.Path("/").Methods(http.MethodGet).HandlerFunc(publicHandler.Index)
|
publicRouter.Path("/").Methods(http.MethodGet).HandlerFunc(publicHandler.GetIndex)
|
||||||
publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(publicHandler.Login)
|
publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(publicHandler.PostLogin)
|
||||||
publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(publicHandler.Logout)
|
publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(publicHandler.PostLogout)
|
||||||
publicRouter.Path("/signup").Methods(http.MethodGet, http.MethodPost).HandlerFunc(publicHandler.Signup)
|
publicRouter.Path("/signup").Methods(http.MethodGet).HandlerFunc(publicHandler.GetSignup)
|
||||||
publicRouter.Path("/imprint").Methods(http.MethodGet).HandlerFunc(publicHandler.Imprint)
|
publicRouter.Path("/signup").Methods(http.MethodPost).HandlerFunc(publicHandler.PostSignup)
|
||||||
|
publicRouter.Path("/imprint").Methods(http.MethodGet).HandlerFunc(publicHandler.GetImprint)
|
||||||
|
|
||||||
// Summary Routes
|
// Summary Routes
|
||||||
summaryRouter.Methods(http.MethodGet).HandlerFunc(summaryHandler.Index)
|
summaryRouter.Methods(http.MethodGet).HandlerFunc(summaryHandler.GetIndex)
|
||||||
|
|
||||||
|
// Settings Routes
|
||||||
|
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
|
||||||
|
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
|
||||||
|
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
|
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
|
||||||
|
@ -19,8 +19,27 @@ type Signup struct {
|
|||||||
PasswordRepeat string `schema:"password_repeat"`
|
PasswordRepeat string `schema:"password_repeat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CredentialsReset struct {
|
||||||
|
PasswordOld string `schema:"password_old"`
|
||||||
|
PasswordNew string `schema:"password_new"`
|
||||||
|
PasswordRepeat string `schema:"password_repeat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CredentialsReset) IsValid() bool {
|
||||||
|
return validatePassword(c.PasswordNew) &&
|
||||||
|
c.PasswordNew == c.PasswordRepeat
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Signup) IsValid() bool {
|
func (s *Signup) IsValid() bool {
|
||||||
return len(s.Username) >= 3 &&
|
return validateUsername(s.Username) &&
|
||||||
len(s.Password) >= 6 &&
|
validatePassword(s.Password) &&
|
||||||
s.Password == s.PasswordRepeat
|
s.Password == s.PasswordRepeat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateUsername(username string) bool {
|
||||||
|
return len(username) >= 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePassword(password string) bool {
|
||||||
|
return len(password) >= 6
|
||||||
|
}
|
||||||
|
@ -29,7 +29,7 @@ func NewIndexHandler(userService *services.UserService, keyValueService *service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) Index(w http.ResponseWriter, r *http.Request) {
|
func (h *IndexHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ func (h *IndexHandler) Index(w http.ResponseWriter, r *http.Request) {
|
|||||||
templates["index.tpl.html"].Execute(w, nil)
|
templates["index.tpl.html"].Execute(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) Imprint(w http.ResponseWriter, r *http.Request) {
|
func (h *IndexHandler) GetImprint(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ func (h *IndexHandler) Imprint(w http.ResponseWriter, r *http.Request) {
|
|||||||
}{HtmlText: text})
|
}{HtmlText: text})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) Login(w http.ResponseWriter, r *http.Request) {
|
func (h *IndexHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -121,7 +121,7 @@ func (h *IndexHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.BasePath), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.BasePath), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
func (h *IndexHandler) PostLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -130,27 +130,7 @@ func (h *IndexHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, fmt.Sprintf("%s/", h.config.BasePath), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("%s/", h.config.BasePath), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
func (h *IndexHandler) GetSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
|
||||||
loadTemplates()
|
|
||||||
}
|
|
||||||
|
|
||||||
if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" {
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.BasePath), http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handlePostSignup(w, r)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
h.handleGetSignup(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *IndexHandler) handleGetSignup(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -167,7 +147,7 @@ func (h *IndexHandler) handleGetSignup(w http.ResponseWriter, r *http.Request) {
|
|||||||
templates["signup.tpl.html"].Execute(w, nil)
|
templates["signup.tpl.html"].Execute(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) handlePostSignup(w http.ResponseWriter, r *http.Request) {
|
func (h *IndexHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ func respondAlert(w http.ResponseWriter, error, success, tplName string, status
|
|||||||
templates[tplName].Execute(w, struct {
|
templates[tplName].Execute(w, struct {
|
||||||
Error string
|
Error string
|
||||||
Success string
|
Success string
|
||||||
}{Error: error})
|
}{Error: error, Success: success})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: do better
|
// TODO: do better
|
||||||
|
97
routes/settings.go
Normal file
97
routes/settings.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
|
"github.com/muety/wakapi/utils"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SettingsHandler struct {
|
||||||
|
config *models.Config
|
||||||
|
userSrvc *services.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
var credentialsDecoder = schema.NewDecoder()
|
||||||
|
|
||||||
|
func NewSettingsHandler(userService *services.UserService) *SettingsHandler {
|
||||||
|
return &SettingsHandler{
|
||||||
|
config: models.GetConfig(),
|
||||||
|
userSrvc: userService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
templates["settings.tpl.html"].Execute(w, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
|
||||||
|
var credentials models.CredentialsReset
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
respondAlert(w, "missing parameters", "", "settings.tpl.html", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := credentialsDecoder.Decode(&credentials, r.PostForm); err != nil {
|
||||||
|
respondAlert(w, "missing parameters", "", "settings.tpl.html", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.CheckPasswordBcrypt(user, credentials.PasswordOld, h.config.PasswordSalt) {
|
||||||
|
respondAlert(w, "invalid credentials", "", "settings.tpl.html", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !credentials.IsValid() {
|
||||||
|
respondAlert(w, "invalid parameters", "", "settings.tpl.html", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = credentials.PasswordNew
|
||||||
|
if err := utils.HashPassword(user, h.config.PasswordSalt); err != nil {
|
||||||
|
respondAlert(w, "internal server error", "", "settings.tpl.html", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.userSrvc.Update(user); err != nil {
|
||||||
|
respondAlert(w, "internal server error", "", "settings.tpl.html", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
login := &models.Login{
|
||||||
|
Username: user.ID,
|
||||||
|
Password: user.Password,
|
||||||
|
}
|
||||||
|
encoded, err := h.config.SecureCookie.Encode(models.AuthCookieKey, login)
|
||||||
|
if err != nil {
|
||||||
|
respondAlert(w, "internal server error", "", "settings.tpl.html", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: models.AuthCookieKey,
|
||||||
|
Value: encoded,
|
||||||
|
Path: "/",
|
||||||
|
Secure: !h.config.InsecureCookies,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("%s/settings", h.config.BasePath), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,7 @@ func (h *SummaryHandler) ApiGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.RespondJSON(w, http.StatusOK, summary)
|
utils.RespondJSON(w, http.StatusOK, summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) {
|
func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
1.6.3
|
1.7.0
|
@ -5,7 +5,7 @@
|
|||||||
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
||||||
<div><a href="" class="text-gray-500 text-sm">🠐 Go back</a></div>
|
<div><a href="" class="text-gray-500 text-sm">&larr Go back</a></div>
|
||||||
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Imprint & Data Privacy</h1>
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Imprint & Data Privacy</h1>
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
55
views/settings.tpl.html
Normal file
55
views/settings.tpl.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
{{ template "head.tpl.html" . }}
|
||||||
|
|
||||||
|
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||||
|
<div class="w-full flex justify-center">
|
||||||
|
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
||||||
|
<div><a href="" class="text-gray-500 text-sm">← Go back</a></div>
|
||||||
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Settings</h1></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "alerts.tpl.html" . }}
|
||||||
|
|
||||||
|
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||||
|
<div class="flex-grow max-w-lg mt-8">
|
||||||
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
||||||
|
Change Password
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="mt-10" action="settings/credentials" method="post">
|
||||||
|
<div class="mb-8">
|
||||||
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password_old">Current Password</label>
|
||||||
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
|
type="password" id="password_old"
|
||||||
|
name="password_old" placeholder="Enter your old password" minlength="6" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-8">
|
||||||
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password_new">New Password</label>
|
||||||
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
|
type="password" id="password_new"
|
||||||
|
name="password_new" placeholder="Choose a password" minlength="6" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-8">
|
||||||
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password_repeat">And again ...</label>
|
||||||
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
|
type="password" id="password_repeat"
|
||||||
|
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between float-right">
|
||||||
|
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ template "footer.tpl.html" . }}
|
||||||
|
|
||||||
|
{{ template "foot.tpl.html" . }}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -5,7 +5,7 @@
|
|||||||
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
||||||
<div><a href="" class="text-gray-500 text-sm">🠐 Go back</a></div>
|
<div><a href="" class="text-gray-500 text-sm">&larr Go back</a></div>
|
||||||
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Sign Up</h1></div>
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Sign Up</h1></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
name="password" placeholder="Choose a password" minlength="6" required>
|
name="password" placeholder="Choose a password" minlength="6" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="password">And again ...</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password_repeat">And again ...</label>
|
||||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
type="password" id="password_repeat"
|
type="password" id="password_repeat"
|
||||||
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
|
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
|
||||||
|
@ -4,21 +4,32 @@
|
|||||||
|
|
||||||
<body class="relative bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
<body class="relative bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||||
|
|
||||||
<div class="hidden flex bg-gray-800 shadow-md z-10 p-2 absolute top-0 right-0 mt-10 mr-8 border border-green-700 rounded popup" id="api-key-popup">
|
<div class="hidden flex bg-gray-800 shadow-md z-10 p-2 absolute top-0 right-0 mt-10 mr-8 border border-green-700 rounded popup"
|
||||||
|
id="api-key-popup">
|
||||||
<div class="flex-grow flex flex-col px-2">
|
<div class="flex-grow flex flex-col px-2">
|
||||||
<span class="text-xs text-gray-500 mx-1">API Key</span>
|
<span class="text-xs text-gray-500 mx-1">API Key</span>
|
||||||
<input type="text" class="bg-transparent text-sm text-white mx-1 font-mono" id="api-key-container" readonly value="{{ .ApiKey }}" style="min-width: 330px">
|
<input type="text" class="bg-transparent text-sm text-white mx-1 font-mono" id="api-key-container" readonly
|
||||||
|
value="{{ .ApiKey }}" style="min-width: 330px">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-2 border-l border-gray-700">
|
<div class="flex items-center px-2 border-l border-gray-700">
|
||||||
<button title="Copy to clipboard" onclick="copyApiKey(event)">📋</button>
|
<button title="Copy to clipboard" onclick="copyApiKey(event)">📋</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-0 right-0 mr-8 mt-10 py-2">
|
<div class="absolute flex top-0 right-0 mr-8 mt-10 py-2">
|
||||||
<form action="logout" method="post">
|
<div class="mx-1">
|
||||||
<button type="button" class="mx-1 py-1 px-3 rounded border border-green-700 text-white text-sm" onclick="showApiKeyPopup(event)">🔐</button>
|
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm"
|
||||||
<button type="submit" class="mx-1 py-1 px-3 rounded border border-green-700 text-white text-sm">Logout</button>
|
onclick="showApiKeyPopup(event)">🔐
|
||||||
</form>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mx-1">
|
||||||
|
<a href="settings" class="py-1 px-3 h-8 block rounded border border-green-700 text-white text-sm">⚙️</a>
|
||||||
|
</div>
|
||||||
|
<div class="mx-1">
|
||||||
|
<form action="logout" method="post">
|
||||||
|
<button type="submit" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user