(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 ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"time" "time"
@ -13,13 +14,22 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type TemplateData struct { type loginRegisterResponse struct {
Account *model.Account Account *model.Account
Message string Message string
Token string Token string
} }
func AccountHandler(app *model.AppState) http.Handler { 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
account := r.Context().Value("account").(*model.Account) 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
type AccountResponse struct { type accountResponse struct {
Account *model.Account Account *model.Account
TOTPs []model.TOTP TOTPs []model.TOTP
Message string
Error string
} }
err = pages["account"].Execute(w, AccountResponse{ err = pages["account"].Execute(w, accountResponse{
Account: account, Account: account,
TOTPs: totps, TOTPs: totps,
Message: r.URL.Query().Get("message"),
Error: r.URL.Query().Get("error"),
}) })
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to render admin account page: %v\n", err) fmt.Printf("WARN: Failed to render admin account page: %v\n", err)
@ -59,7 +73,7 @@ func LoginHandler(app *model.AppState) http.Handler {
return return
} }
err = pages["login"].Execute(w, TemplateData{}) err = pages["login"].Execute(w, loginRegisterResponse{})
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -213,12 +227,12 @@ func createAccountHandler(app *model.AppState) http.Handler {
return return
} }
type CreateAccountResponse struct { type CreateaccountResponse struct {
Account *model.Account Account *model.Account
Message string Message string
} }
render := func(data CreateAccountResponse) { render := func(data CreateaccountResponse) {
err := pages["create-account"].Execute(w, data) err := pages["create-account"].Execute(w, data)
if err != nil { if err != nil {
fmt.Printf("WARN: Error rendering create account page: %s\n", err) 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 { if r.Method == http.MethodGet {
render(CreateAccountResponse{}) render(CreateaccountResponse{})
return return
} }
@ -238,7 +252,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
err = r.ParseForm() err = r.ParseForm()
if err != nil { if err != nil {
render(CreateAccountResponse{ render(CreateaccountResponse{
Message: "Malformed data.", Message: "Malformed data.",
}) })
return return
@ -261,7 +275,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
invite, err := controller.GetInvite(app.DB, credentials.Invite) invite, err := controller.GetInvite(app.DB, credentials.Invite)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err)
render(CreateAccountResponse{ render(CreateaccountResponse{
Message: "Something went wrong. Please try again.", Message: "Something went wrong. Please try again.",
}) })
return return
@ -271,7 +285,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
err := controller.DeleteInvite(app.DB, invite.Code) err := controller.DeleteInvite(app.DB, invite.Code)
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) }
} }
render(CreateAccountResponse{ render(CreateaccountResponse{
Message: "Invalid invite code.", Message: "Invalid invite code.",
}) })
return return
@ -280,7 +294,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err)
render(CreateAccountResponse{ render(CreateaccountResponse{
Message: "Something went wrong. Please try again.", Message: "Something went wrong. Please try again.",
}) })
return return
@ -295,13 +309,13 @@ func createAccountHandler(app *model.AppState) http.Handler {
err = controller.CreateAccount(app.DB, &account) err = controller.CreateAccount(app.DB, &account)
if err != nil { if err != nil {
if strings.HasPrefix(err.Error(), "pq: duplicate key") { if strings.HasPrefix(err.Error(), "pq: duplicate key") {
render(CreateAccountResponse{ render(CreateaccountResponse{
Message: "An account with that username already exists.", Message: "An account with that username already exists.",
}) })
return return
} }
fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err)
render(CreateAccountResponse{ render(CreateaccountResponse{
Message: "Something went wrong. Please try again.", Message: "Something went wrong. Please try again.",
}) })
return return
@ -330,7 +344,7 @@ func createAccountHandler(app *model.AppState) http.Handler {
cookie.Path = "/" cookie.Path = "/"
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
err = pages["login"].Execute(w, TemplateData{ err = pages["login"].Execute(w, loginRegisterResponse{
Account: &account, Account: &account,
Token: token.Token, Token: token.Token,
}) })
@ -341,3 +355,30 @@ func createAccountHandler(app *model.AppState) http.Handler {
} }
}) })
} }
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("/login", LoginHandler(app))
mux.Handle("/register", createAccountHandler(app)) mux.Handle("/register", createAccountHandler(app))
mux.Handle("/logout", RequireAccount(app, LogoutHandler(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("/static/", http.StripPrefix("/static", staticHandler()))
mux.Handle("/release/", RequireAccount(app, http.StripPrefix("/release", serveRelease(app)))) mux.Handle("/release/", RequireAccount(app, http.StripPrefix("/release", serveRelease(app))))
mux.Handle("/artist/", RequireAccount(app, http.StripPrefix("/artist", serveArtist(app)))) mux.Handle("/artist/", RequireAccount(app, http.StripPrefix("/artist", serveArtist(app))))

View file

@ -6,22 +6,28 @@
{{define "content"}} {{define "content"}}
<main> <main>
{{if .Message}}
<p id="message">{{.Message}}</p>
{{end}}
{{if .Error}}
<p id="error">{{.Error}}</p>
{{end}}
<h1>Account Settings ({{.Account.Username}})</h1> <h1>Account Settings ({{.Account.Username}})</h1>
<div class="card-title"> <div class="card-title">
<h2>Change Password</h2> <h2>Change Password</h2>
</div> </div>
<div class="card"> <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> <div>
<label for="current-password">Current Password</label> <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> <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> <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> </div>
<button type="submit" class="save">Change Password</button> <button type="submit" class="save">Change Password</button>

View file

@ -26,7 +26,10 @@
<div class="flex-fill"></div> <div class="flex-fill"></div>
{{if .Account}} {{if .Account}}
<div class="nav-item"> <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> </div>
{{else}} {{else}}
<div class="nav-item"> <div class="nav-item">

View file

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

View file

@ -338,9 +338,9 @@ func main() {
// start the web server! // start the web server!
mux := createServeMux(&app) 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( 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)), HTTPLog(DefaultHeaders(mux)),
)) ))
} }

View file

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

View file

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