(incomplete) change password feature

This commit is contained in:
ari melody 2025-01-21 17:13:06 +00:00
parent 5531ef6bab
commit 0052c470f9
Signed by: ari
GPG key ID: CF99829C92678188
8 changed files with 76 additions and 25 deletions

View file

@ -3,6 +3,7 @@ package admin
import (
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
@ -13,13 +14,22 @@ import (
"golang.org/x/crypto/bcrypt"
)
type TemplateData struct {
type loginRegisterResponse struct {
Account *model.Account
Message string
Token string
}
func AccountHandler(app *model.AppState) http.Handler {
mux := http.NewServeMux()
mux.Handle("/password", changePasswordHandler(app))
mux.Handle("/", accountIndexHandler(app))
return mux
}
func accountIndexHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
account := r.Context().Value("account").(*model.Account)
@ -29,14 +39,18 @@ func AccountHandler(app *model.AppState) http.Handler {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
type AccountResponse struct {
type accountResponse struct {
Account *model.Account
TOTPs []model.TOTP
Message string
Error string
}
err = pages["account"].Execute(w, AccountResponse{
err = pages["account"].Execute(w, accountResponse{
Account: account,
TOTPs: totps,
Message: r.URL.Query().Get("message"),
Error: r.URL.Query().Get("error"),
})
if err != nil {
fmt.Printf("WARN: Failed to render admin account page: %v\n", err)
@ -59,7 +73,7 @@ func LoginHandler(app *model.AppState) http.Handler {
return
}
err = pages["login"].Execute(w, TemplateData{})
err = pages["login"].Execute(w, loginRegisterResponse{})
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -213,12 +227,12 @@ func createAccountHandler(app *model.AppState) http.Handler {
return
}
type CreateAccountResponse struct {
type CreateaccountResponse struct {
Account *model.Account
Message string
}
render := func(data CreateAccountResponse) {
render := func(data CreateaccountResponse) {
err := pages["create-account"].Execute(w, data)
if err != nil {
fmt.Printf("WARN: Error rendering create account page: %s\n", err)
@ -227,7 +241,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
}
if r.Method == http.MethodGet {
render(CreateAccountResponse{})
render(CreateaccountResponse{})
return
}
@ -238,7 +252,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
err = r.ParseForm()
if err != nil {
render(CreateAccountResponse{
render(CreateaccountResponse{
Message: "Malformed data.",
})
return
@ -261,7 +275,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
invite, err := controller.GetInvite(app.DB, credentials.Invite)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err)
render(CreateAccountResponse{
render(CreateaccountResponse{
Message: "Something went wrong. Please try again.",
})
return
@ -271,7 +285,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
err := controller.DeleteInvite(app.DB, invite.Code)
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) }
}
render(CreateAccountResponse{
render(CreateaccountResponse{
Message: "Invalid invite code.",
})
return
@ -280,7 +294,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err)
render(CreateAccountResponse{
render(CreateaccountResponse{
Message: "Something went wrong. Please try again.",
})
return
@ -295,13 +309,13 @@ func createAccountHandler(app *model.AppState) http.Handler {
err = controller.CreateAccount(app.DB, &account)
if err != nil {
if strings.HasPrefix(err.Error(), "pq: duplicate key") {
render(CreateAccountResponse{
render(CreateaccountResponse{
Message: "An account with that username already exists.",
})
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err)
render(CreateAccountResponse{
render(CreateaccountResponse{
Message: "Something went wrong. Please try again.",
})
return
@ -330,14 +344,41 @@ func createAccountHandler(app *model.AppState) http.Handler {
cookie.Path = "/"
http.SetCookie(w, &cookie)
err = pages["login"].Execute(w, TemplateData{
err = pages["login"].Execute(w, loginRegisterResponse{
Account: &account,
Token: token.Token,
})
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render login page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func changePasswordHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
account := r.Context().Value("account").(*model.Account)
r.ParseForm()
currentPassword := r.Form.Get("current-password")
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(currentPassword)); err != nil {
http.Redirect(w, r, "/admin/account?error=" + url.PathEscape("Incorrect password."), http.StatusFound)
return
}
newPassword := r.Form.Get("new-password")
http.Redirect(
w, r, "/admin/account?message=" +
url.PathEscape(fmt.Sprintf("Updating password to <code>%s</code>", newPassword)),
http.StatusFound,
)
})
}

View file

@ -17,7 +17,7 @@ func Handler(app *model.AppState) http.Handler {
mux.Handle("/login", LoginHandler(app))
mux.Handle("/register", createAccountHandler(app))
mux.Handle("/logout", RequireAccount(app, LogoutHandler(app)))
mux.Handle("/account", RequireAccount(app, AccountHandler(app)))
mux.Handle("/account/", RequireAccount(app, http.StripPrefix("/account", AccountHandler(app))))
mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
mux.Handle("/release/", RequireAccount(app, http.StripPrefix("/release", serveRelease(app))))
mux.Handle("/artist/", RequireAccount(app, http.StripPrefix("/artist", serveArtist(app))))

View file

@ -6,22 +6,28 @@
{{define "content"}}
<main>
{{if .Message}}
<p id="message">{{.Message}}</p>
{{end}}
{{if .Error}}
<p id="error">{{.Error}}</p>
{{end}}
<h1>Account Settings ({{.Account.Username}})</h1>
<div class="card-title">
<h2>Change Password</h2>
</div>
<div class="card">
<form action="/api/v1/change-password" method="POST" id="change-password">
<form action="/admin/account/password" method="POST" id="change-password">
<div>
<label for="current-password">Current Password</label>
<input type="password" name="current-password" value="" autocomplete="current-password">
<input type="password" id="current-password" name="current-password" value="" autocomplete="current-password">
<label for="new-password">Password</label>
<input type="password" name="new-password" value="" autocomplete="new-password">
<input type="password" id="new-password" name="new-password" value="" autocomplete="new-password">
<label for="confirm-password">Confirm Password</label>
<input type="password" name="confirm-password" value="" autocomplete="new-password">
<input type="password" id="confirm-password" value="" autocomplete="new-password">
</div>
<button type="submit" class="save">Change Password</button>

View file

@ -26,7 +26,10 @@
<div class="flex-fill"></div>
{{if .Account}}
<div class="nav-item">
<a href="/admin/logout" id="logout">logged in as {{.Account.Username}}. log out</a>
<a href="/admin/account">account ({{.Account.Username}})</a>
</div>
<div class="nav-item">
<a href="/admin/logout" id="logout">log out</a>
</div>
{{else}}
<div class="nav-item">

View file

@ -19,6 +19,7 @@ func GetConfig() model.Config {
config := model.Config{
BaseUrl: "https://arimelody.me",
Host: "0.0.0.0",
Port: 8080,
DB: model.DBConfig{
Host: "127.0.0.1",
@ -55,6 +56,7 @@ func handleConfigOverrides(config *model.Config) error {
var err error
if env, has := os.LookupEnv("ARIMELODY_BASE_URL"); has { config.BaseUrl = env }
if env, has := os.LookupEnv("ARIMELODY_HOST"); has { config.Host = env }
if env, has := os.LookupEnv("ARIMELODY_PORT"); has {
config.Port, err = strconv.ParseInt(env, 10, 0)
if err != nil { return errors.New("ARIMELODY_PORT: " + err.Error()) }

View file

@ -338,9 +338,9 @@ func main() {
// start the web server!
mux := createServeMux(&app)
fmt.Printf("Now serving at %s:%d\n", app.Config.BaseUrl, app.Config.Port)
fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
log.Fatal(
http.ListenAndServe(fmt.Sprintf(":%d", app.Config.Port),
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
HTTPLog(DefaultHeaders(mux)),
))
}

View file

@ -19,6 +19,7 @@ type (
Config struct {
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
Host string `toml:"host"`
Port int64 `toml:"port"`
DataDirectory string `toml:"data_dir"`
DB DBConfig `toml:"db"`

View file

@ -1,5 +1,3 @@
CREATE SCHEMA IF NOT EXISTS arimelody;
--
-- Tables
--