1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

Compare commits

..

13 Commits
1.6.2 ... 1.7.1

Author SHA1 Message Date
4f035b3a63 chore: minor code improvement 2020-06-07 20:01:31 +02:00
0eac9a8854 feat: add ability to reset api key (resolve #29) 2020-06-07 19:58:06 +02:00
0294425de0 feat: add ability to change passwords (resolve #30) 2020-06-07 19:28:32 +02:00
a7c83252ef debug: re-add workflows again 2020-05-31 10:29:15 +02:00
07a03ce3ac debug: remove workflows 2020-05-31 10:28:55 +02:00
160c2f713e chore: update version count 2020-05-31 09:54:02 +02:00
05b740c87d chore: minor changes to the github actions 2020-05-31 09:52:13 +02:00
274be6caf8 chore: run in dev mode by default 2020-05-31 09:51:55 +02:00
58fef96f22 Merge branch 'LightPOS-win-gh-actions-build' 2020-05-31 09:38:38 +02:00
629a3212c7 feat: persist user creation date (resolve #31) 2020-05-31 09:38:26 +02:00
0a513e959b Automatically build the project for Linux users
Add a GitHub Action to build on Linux when a release is created
2020-05-30 21:50:16 +01:00
c68ee0a81e Remove upload of artifact to Actions' artifacts
There is no need to upload the artifact to the Action itself since it will be uploaded to the release.
2020-05-30 21:04:22 +01:00
e4a2fbd51a Automatically build the project for Windows users
This change makes it simpler for Windows users to use the project by automatically building the project with GitHub Actions on every release.

This allows for an easier way to use the project by automatically adding a zip file with the built executables to new releases.
2020-05-30 21:01:54 +01:00
16 changed files with 350 additions and 56 deletions

View File

@ -1,4 +1,4 @@
ENV=prod ENV=dev
WAKAPI_DB_TYPE=sqlite3 # mysql, postgres, sqlite3 WAKAPI_DB_TYPE=sqlite3 # mysql, postgres, sqlite3
WAKAPI_DB_NAME=wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db) WAKAPI_DB_NAME=wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
WAKAPI_DB_USER=myuser # ignored when using sqlite WAKAPI_DB_USER=myuser # ignored when using sqlite

View File

@ -0,0 +1,43 @@
name: Build Wakapi on Linux
on:
release:
types:
- created
jobs:
build-and-release:
name: Build and add to Release
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Build
run: GO111MODULE=on go build -v .
- name: Zip Release
uses: TheDoctor0/zip-release@v0.3.0
with:
filename: release.zip
- name: Upload built executable to Release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: release.zip
asset_name: wakapi_linux_amd64.zip
asset_content_type: application/gzip

View File

@ -0,0 +1,44 @@
name: Build Wakapi on Windows
on:
release:
types:
- created
jobs:
build-and-release:
name: Build and add to release
runs-on: windows-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Enable Go 1.11 modules
run: cmd /c "set GO111MODULE=on"
- name: Build
run: go build -v .
- name: Compress working folder
run: Compress-Archive -Path .\* -DestinationPath release.zip
- name: Upload built executable to Release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: release.zip
asset_name: wakapi_win_amd64.zip
asset_content_type: application/gzip

View File

@ -40,6 +40,10 @@ To use the demo version set `api_url = https://apps.muetsch.io/wakapi/api/heartb
1. Build executable: `GO111MODULE=on go build` 1. Build executable: `GO111MODULE=on go build`
1. Run server: `./wakapi` 1. Run server: `./wakapi`
**As an alternative** to building from source you can also grab a pre-built [release](https://github.com/muety/wakapi/releases). Steps 2, 3 and 5 apply analogously.
**Note:** By default, the application is running in dev mode. However, it is recommended to set `ENV=production` in `.env` for enhanced performance and security. To still be able to log in when using production mode, you either have to run Wakapi behind a reverse proxy, that enables for HTTPS encryption (see [best practices](i#best-practices)) or set `insecure_cookies = true` in `config.ini`.
### Run with Docker ### Run with Docker
``` ```
docker run -d -p 3000:3000 --name wakapi n1try/wakapi docker run -d -p 3000:3000 --name wakapi n1try/wakapi

21
main.go
View File

@ -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)

View File

@ -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
}

View File

@ -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()
} }
@ -43,18 +43,10 @@ func (h *IndexHandler) Index(w http.ResponseWriter, r *http.Request) {
return return
} }
// TODO: make this more generic and reusable
if success := r.URL.Query().Get("success"); success != "" {
templates["index.tpl.html"].Execute(w, struct {
Success string
Error string
}{Success: success})
return
}
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 +61,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 +113,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 +122,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 +139,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()
} }

View File

@ -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

112
routes/settings.go Normal file
View File

@ -0,0 +1,112 @@
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"
"net/url"
)
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()
}
if handleAlerts(w, r, "settings.tpl.html") {
return
}
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)
msg := url.QueryEscape("password was updated successfully")
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.BasePath, msg), http.StatusFound)
}
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
if _, err := h.userSrvc.ResetApiKey(user); err != nil {
respondAlert(w, "internal server error", "", "settings.tpl.html", http.StatusInternalServerError)
return
}
msg := url.QueryEscape(fmt.Sprintf("your new api key is: %s", user.ApiKey))
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.BasePath, msg), http.StatusFound)
}

View File

@ -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()
} }

View File

@ -82,6 +82,11 @@ func (srv *UserService) Update(user *models.User) (*models.User, error) {
return user, nil return user, nil
} }
func (srv *UserService) ResetApiKey(user *models.User) (*models.User, error) {
user.ApiKey = uuid.NewV4().String()
return srv.Update(user)
}
func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) { func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) {
user.Password = login.Password user.Password = login.Password
if err := utils.HashPassword(user, srv.Config.PasswordSalt); err != nil { if err := utils.HashPassword(user, srv.Config.PasswordSalt); err != nil {

View File

@ -1 +1 @@
1.6.2 1.7.1

View File

@ -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>

75
views/settings.tpl.html Normal file
View File

@ -0,0 +1,75 @@
<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">&larr; 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-4 flex-grow flex justify-center w-full">
<div class="flex flex-col flex-grow max-w-lg mt-8">
<div class="w-full my-8 pb-8 border-b border-gray-700">
<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>
<div class="w-full mt-4 mb-8 pb-8">
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
Reset API Key
</div>
<form class="mt-6" action="settings/reset" method="post">
<div class="text-gray-300 text-sm mb-4">
<strong>⚠️ Caution:</strong> Resetting your API key requires you to update your <span class="font-mono">.wakatime.cfg</span> files on all of your computers to make the WakaTime client send heartbeats again.
</div>
<div class="flex justify-between float-right">
<button type="submit" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm">
Reset
</button>
</div>
</form>
</div>
</div>
</main>
{{ template "footer.tpl.html" . }}
{{ template "foot.tpl.html" . }}
</body>
</html>

View File

@ -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>

View File

@ -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">