Compare commits
6 Commits
a2932d7b1c
...
3509cb9666
| Author | SHA1 | Date |
|---|---|---|
|
|
3509cb9666 | |
|
|
f110283b8e | |
|
|
1980e33d0f | |
|
|
55e6be7239 | |
|
|
b57652e6d2 | |
|
|
3cfeca65fc |
|
|
@ -1,9 +1,9 @@
|
||||||
package lishwist // import "lishwist/core"
|
package lishwist // import "."
|
||||||
|
|
||||||
|
|
||||||
VARIABLES
|
VARIABLES
|
||||||
|
|
||||||
var ErrorUsernameTaken = errors.New("Username is taken")
|
var ErrorUsernameTaken = errors.New("username is taken")
|
||||||
|
|
||||||
FUNCTIONS
|
FUNCTIONS
|
||||||
|
|
||||||
|
|
@ -31,6 +31,8 @@ func (a *Admin) RemoveUserFromGroup(userId, groupId string) error
|
||||||
|
|
||||||
func (u *Admin) RenameUser(userReference string, displayName string) error
|
func (u *Admin) RenameUser(userReference string, displayName string) error
|
||||||
|
|
||||||
|
func (u *Admin) SetUserPassword(userReference string, newPassword string) error
|
||||||
|
|
||||||
func (u *Admin) UserSetLive(userReference string, setting bool) error
|
func (u *Admin) UserSetLive(userReference string, setting bool) error
|
||||||
|
|
||||||
type ErrorInvalidCredentials error
|
type ErrorInvalidCredentials error
|
||||||
|
|
@ -81,12 +83,13 @@ func (u *Session) SuggestWishForUser(otherUserReference string, wishName string)
|
||||||
func (s *Session) User() User
|
func (s *Session) User() User
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Id string
|
Id string
|
||||||
NormalName string
|
NormalName string
|
||||||
Name string
|
Name string
|
||||||
Reference string
|
Reference string
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
IsLive bool
|
IsLive bool
|
||||||
|
PasswordFromAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByReference(reference string) (*User, error)
|
func GetUserByReference(reference string) (*User, error)
|
||||||
|
|
@ -95,6 +98,8 @@ func Register(username, newPassword string) (*User, error)
|
||||||
|
|
||||||
func (u *User) GetTodo() ([]Wish, error)
|
func (u *User) GetTodo() ([]Wish, error)
|
||||||
|
|
||||||
|
func (u *User) SetPassword(newPassword string) error
|
||||||
|
|
||||||
func (u *User) WishCount() (int, error)
|
func (u *User) WishCount() (int, error)
|
||||||
|
|
||||||
type Wish struct {
|
type Wish struct {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||||
"password_hash" TEXT NOT NULL,
|
"password_hash" TEXT NOT NULL,
|
||||||
"is_admin" INTEGER NOT NULL DEFAULT 0,
|
"is_admin" INTEGER NOT NULL DEFAULT 0,
|
||||||
"is_live" INTEGER NOT NULL DEFAULT 1,
|
"is_live" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"password_from_admin" INTEGER NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY("id" AUTOINCREMENT)
|
PRIMARY KEY("id" AUTOINCREMENT)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "wish" (
|
CREATE TABLE IF NOT EXISTS "wish" (
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrorUsernameTaken = errors.New("Username is taken")
|
var ErrorUsernameTaken = errors.New("username is taken")
|
||||||
|
|
||||||
func Register(username, newPassword string) (*User, error) {
|
func Register(username, newPassword string) (*User, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
|
|
@ -24,17 +24,17 @@ func Register(username, newPassword string) (*User, error) {
|
||||||
|
|
||||||
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
|
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to hash password: %w", err)
|
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
usersExist, err := hasUsers()
|
usersExist, err := hasUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to count users: %w", err)
|
return nil, fmt.Errorf("failed to count users: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := createUser(username, hashedPasswordBytes, !usersExist)
|
user, err := createUser(username, hashedPasswordBytes, !usersExist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to create user: %w\n", err)
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func (s *Session) User() User {
|
||||||
|
|
||||||
func SessionFromKey(key string) (*Session, error) {
|
func SessionFromKey(key string) (*Session, error) {
|
||||||
s := Session{}
|
s := Session{}
|
||||||
query := "SELECT user.id, user.name, user.display_name, user.reference, user.is_admin, user.is_live, session.key, session.expiry FROM v_user as user JOIN session ON user.id = session.user_id WHERE session.key = ?"
|
query := "SELECT user.id, user.name, user.display_name, user.reference, user.is_admin, user.is_live, user.password_from_admin, session.key, session.expiry FROM v_user as user JOIN session ON user.id = session.user_id WHERE session.key = ?"
|
||||||
var expiry string
|
var expiry string
|
||||||
err := db.Connection.QueryRow(query, key).Scan(
|
err := db.Connection.QueryRow(query, key).Scan(
|
||||||
&s.user.Id,
|
&s.user.Id,
|
||||||
|
|
@ -32,6 +32,7 @@ func SessionFromKey(key string) (*Session, error) {
|
||||||
&s.user.Reference,
|
&s.user.Reference,
|
||||||
&s.user.IsAdmin,
|
&s.user.IsAdmin,
|
||||||
&s.user.IsLive,
|
&s.user.IsLive,
|
||||||
|
&s.user.PasswordFromAdmin,
|
||||||
&s.Key,
|
&s.Key,
|
||||||
&expiry,
|
&expiry,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
51
core/user.go
51
core/user.go
|
|
@ -4,19 +4,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"lishwist/core/internal/db"
|
"lishwist/core/internal/db"
|
||||||
"lishwist/core/internal/normalize"
|
"lishwist/core/internal/normalize"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Id string
|
Id string
|
||||||
NormalName string
|
NormalName string
|
||||||
// TODO: rename to DisplayName
|
Name string
|
||||||
Name string
|
Reference string
|
||||||
Reference string
|
IsAdmin bool
|
||||||
IsAdmin bool
|
IsLive bool
|
||||||
IsLive bool
|
PasswordFromAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryManyUsers(query string, args ...any) ([]User, error) {
|
func queryManyUsers(query string, args ...any) ([]User, error) {
|
||||||
|
|
@ -28,7 +29,7 @@ func queryManyUsers(query string, args ...any) ([]User, error) {
|
||||||
users := []User{}
|
users := []User{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var u User
|
var u User
|
||||||
err = rows.Scan(&u.Id, &u.NormalName, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive)
|
err = rows.Scan(&u.Id, &u.NormalName, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive, &u.PasswordFromAdmin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +55,7 @@ func queryOneUser(query string, args ...any) (*User, error) {
|
||||||
|
|
||||||
func getUserByName(username string) (*User, error) {
|
func getUserByName(username string) (*User, error) {
|
||||||
username = normalize.Name(username)
|
username = normalize.Name(username)
|
||||||
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE name = ?"
|
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM v_user WHERE name = ?"
|
||||||
return queryOneUser(stmt, username)
|
return queryOneUser(stmt, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,12 +92,12 @@ func (u *User) getPassHash() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserByReference(reference string) (*User, error) {
|
func getUserByReference(reference string) (*User, error) {
|
||||||
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE reference = ?"
|
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM v_user WHERE reference = ?"
|
||||||
return queryOneUser(stmt, reference)
|
return queryOneUser(stmt, reference)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserById(id string) (*User, error) {
|
func getUserById(id string) (*User, error) {
|
||||||
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE id = ?"
|
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM v_user WHERE id = ?"
|
||||||
return queryOneUser(stmt, id)
|
return queryOneUser(stmt, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +112,7 @@ func hasUsers() (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Admin) ListUsers() ([]User, error) {
|
func (*Admin) ListUsers() ([]User, error) {
|
||||||
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM user"
|
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM user"
|
||||||
return queryManyUsers(stmt)
|
return queryManyUsers(stmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,3 +173,29 @@ func (u *Admin) RenameUser(userReference string, displayName string) error {
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Admin) SetUserPassword(userReference string, newPassword string) error {
|
||||||
|
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
query := "UPDATE user SET password_hash = ?, password_from_admin = 1 WHERE reference = ?"
|
||||||
|
_, err = db.Connection.Exec(query, hashedPasswordBytes, userReference)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) SetPassword(newPassword string) error {
|
||||||
|
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
query := "UPDATE user SET password_hash = ?, password_from_admin = 0 WHERE id = ?"
|
||||||
|
_, err = db.Connection.Exec(query, hashedPasswordBytes, u.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
type LoginProps struct {
|
type LoginProps struct {
|
||||||
GeneralError string `json:",omitempty"`
|
GeneralError string `json:",omitempty"`
|
||||||
SuccessfulRegistration bool `json:",omitempty"`
|
SuccessfulRegistration bool `json:",omitempty"`
|
||||||
|
SuccessfulSetPassword bool `json:",omitempty"`
|
||||||
Username templates.InputProps
|
Username templates.InputProps
|
||||||
Password templates.InputProps
|
Password templates.InputProps
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
top_level=$(git rev-parse --show-toplevel)
|
||||||
|
git_version=$($top_level/scripts/git-version)
|
||||||
|
|
||||||
|
go run -ldflags=-X=lishwist/http/env.GitVersion=$git_version main.go
|
||||||
|
|
@ -14,20 +14,34 @@ func GuaranteeEnv(key string) string {
|
||||||
return variable
|
return variable
|
||||||
}
|
}
|
||||||
|
|
||||||
var DatabaseFile = GuaranteeEnv("LISHWIST_DATABASE_FILE")
|
type Config struct {
|
||||||
var SessionSecret = GuaranteeEnv("LISHWIST_SESSION_SECRET")
|
DatabaseFile string
|
||||||
var HostRootUrl = GuaranteeEnv("LISHWIST_HOST_ROOT_URL")
|
SessionSecret string
|
||||||
var HostPort = os.Getenv("LISHWIST_HOST_PORT")
|
HostRootUrl string
|
||||||
var ServePort = GuaranteeEnv("LISHWIST_SERVE_PORT")
|
HostPort string
|
||||||
var InDev = os.Getenv("LISHWIST_IN_DEV") != ""
|
ServePort string
|
||||||
var HostUrl = func() *url.URL {
|
InDev bool
|
||||||
rawUrl := HostRootUrl
|
HostUrl string
|
||||||
if HostPort != "" {
|
}
|
||||||
rawUrl += ":" + HostPort
|
|
||||||
}
|
var Configuration Config
|
||||||
u, err := url.Parse(rawUrl)
|
|
||||||
if err != nil {
|
func init() {
|
||||||
log.Fatalln("Couldn't parse host url:", err)
|
Configuration.DatabaseFile = GuaranteeEnv("LISHWIST_DATABASE_FILE")
|
||||||
}
|
Configuration.SessionSecret = GuaranteeEnv("LISHWIST_SESSION_SECRET")
|
||||||
return u
|
Configuration.HostRootUrl = GuaranteeEnv("LISHWIST_HOST_ROOT_URL")
|
||||||
}()
|
Configuration.HostPort = os.Getenv("LISHWIST_HOST_PORT")
|
||||||
|
Configuration.ServePort = GuaranteeEnv("LISHWIST_SERVE_PORT")
|
||||||
|
Configuration.InDev = os.Getenv("LISHWIST_IN_DEV") != ""
|
||||||
|
Configuration.HostUrl = func() string {
|
||||||
|
rawUrl := Configuration.HostRootUrl
|
||||||
|
if Configuration.HostPort != "" {
|
||||||
|
rawUrl += ":" + Configuration.HostPort
|
||||||
|
}
|
||||||
|
u, err := url.Parse(rawUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Couldn't parse host url:", err)
|
||||||
|
}
|
||||||
|
return u.String()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
var GitVersion string
|
||||||
|
|
@ -10,16 +10,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := lishwist.Init(env.DatabaseFile)
|
err := lishwist.Init(env.Configuration.DatabaseFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to init Lishwist: %s\n", err)
|
log.Fatalf("Failed to init Lishwist: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
useSecureCookies := !env.InDev
|
useSecureCookies := !env.Configuration.InDev
|
||||||
r := server.Create(useSecureCookies)
|
r := server.Create(useSecureCookies)
|
||||||
|
|
||||||
log.Printf("Running at http://127.0.0.1:%s\n", env.ServePort)
|
log.Printf("Running at http://127.0.0.1:%s\n", env.Configuration.ServePort)
|
||||||
err = http.ListenAndServe(":"+env.ServePort, r)
|
err = http.ListenAndServe(":"+env.Configuration.ServePort, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to listen and server:", err)
|
log.Fatalln("Failed to listen and server:", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"lishwist/http/session"
|
"lishwist/http/session"
|
||||||
"lishwist/http/templates"
|
"lishwist/http/templates"
|
||||||
|
"lishwist/http/templates/text"
|
||||||
|
|
||||||
"github.com/Teajey/rsvp"
|
"github.com/Teajey/rsvp"
|
||||||
)
|
)
|
||||||
|
|
@ -18,6 +19,7 @@ type ServeMux struct {
|
||||||
func NewServeMux(store *session.Store) *ServeMux {
|
func NewServeMux(store *session.Store) *ServeMux {
|
||||||
mux := rsvp.NewServeMux()
|
mux := rsvp.NewServeMux()
|
||||||
mux.Config.HtmlTemplate = templates.Template
|
mux.Config.HtmlTemplate = templates.Template
|
||||||
|
mux.Config.TextTemplate = text.Template
|
||||||
return &ServeMux{
|
return &ServeMux{
|
||||||
inner: mux,
|
inner: mux,
|
||||||
store: store,
|
store: store,
|
||||||
|
|
|
||||||
|
|
@ -9,31 +9,28 @@ type Session struct {
|
||||||
written bool
|
written bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) FlashGet() any {
|
const flashKey = "_flash"
|
||||||
list := s.inner.Flashes()
|
|
||||||
if len(list) < 1 {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
s.written = true
|
|
||||||
return list[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) FlashPeek() any {
|
func (s *Session) FlashGet() any {
|
||||||
flash, ok := s.inner.Values["_flash"]
|
val, ok := s.inner.Values[flashKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
list := flash.([]any)
|
delete(s.inner.Values, flashKey)
|
||||||
if len(list) < 1 {
|
s.written = true
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) FlashPeek() any {
|
||||||
|
val, ok := s.inner.Values[flashKey]
|
||||||
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
|
||||||
return list[0]
|
|
||||||
}
|
}
|
||||||
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) FlashSet(value any) {
|
func (s *Session) FlashSet(value any) {
|
||||||
s.inner.AddFlash(value)
|
s.inner.Values[flashKey] = value
|
||||||
s.written = true
|
s.written = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,12 +44,17 @@ func (s *Session) SetValue(key any, value any) {
|
||||||
s.written = true
|
s.written = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Session) RemoveValue(key any) {
|
||||||
|
delete(s.inner.Values, key)
|
||||||
|
s.written = true
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) GetValue(key any) any {
|
func (s *Session) GetValue(key any) any {
|
||||||
return s.inner.Values[key]
|
return s.inner.Values[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearValues() {
|
func (s *Session) ClearValues() {
|
||||||
s.inner.Values = nil
|
s.inner.Values = make(map[any]any)
|
||||||
s.written = true
|
s.written = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
lishwist "lishwist/core"
|
||||||
|
"lishwist/http/api"
|
||||||
|
"lishwist/http/response"
|
||||||
|
"lishwist/http/templates"
|
||||||
|
|
||||||
|
"github.com/Teajey/rsvp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountProps struct {
|
||||||
|
CurrentUsername string
|
||||||
|
GeneralError string `json:",omitempty"`
|
||||||
|
PasswordFromAdmin bool `json:",omitempty"`
|
||||||
|
Password templates.InputProps
|
||||||
|
ConfirmPassword templates.InputProps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AccountProps) Validate() (valid bool) {
|
||||||
|
valid = true
|
||||||
|
|
||||||
|
if p.Password.Value != p.ConfirmPassword.Value {
|
||||||
|
p.ConfirmPassword.Error = "Passwords didn't match"
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Password.Validate() {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.ConfirmPassword.Validate() {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountProps(username string, passwordFromAdmin bool, passwordVal, confirmPassVal string) *AccountProps {
|
||||||
|
return &AccountProps{
|
||||||
|
CurrentUsername: username,
|
||||||
|
PasswordFromAdmin: passwordFromAdmin,
|
||||||
|
Password: templates.InputProps{
|
||||||
|
Type: "password",
|
||||||
|
Name: "new_password",
|
||||||
|
Required: true,
|
||||||
|
MinLength: 5,
|
||||||
|
Value: passwordVal,
|
||||||
|
},
|
||||||
|
ConfirmPassword: templates.InputProps{
|
||||||
|
Type: "password",
|
||||||
|
Name: "confirm_password",
|
||||||
|
Required: true,
|
||||||
|
Value: confirmPassVal,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Account(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
|
user := app.User()
|
||||||
|
props := NewAccountProps(user.Name, user.PasswordFromAdmin, "", "")
|
||||||
|
|
||||||
|
flash := session.FlashGet()
|
||||||
|
|
||||||
|
flashProps, _ := flash.(*AccountProps)
|
||||||
|
if flashProps != nil {
|
||||||
|
props.GeneralError = flashProps.GeneralError
|
||||||
|
props.ConfirmPassword.Error = flashProps.ConfirmPassword.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data("account.gotmpl", props)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AccountPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
|
user := app.User()
|
||||||
|
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||||
|
}
|
||||||
|
|
||||||
|
intent := r.Form.Get("intent")
|
||||||
|
if intent != "set_password" {
|
||||||
|
return response.Error(http.StatusBadRequest, "Invalid intent %q", intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPassword := r.Form.Get("new_password")
|
||||||
|
confirmPassword := r.Form.Get("confirm_password")
|
||||||
|
|
||||||
|
props := NewAccountProps(user.Name, user.PasswordFromAdmin, newPassword, confirmPassword)
|
||||||
|
|
||||||
|
valid := props.Validate()
|
||||||
|
props.Password.Value = ""
|
||||||
|
props.ConfirmPassword.Value = ""
|
||||||
|
if !valid {
|
||||||
|
log.Printf("Invalid account props: %#v\n", props)
|
||||||
|
session.FlashSet(&props)
|
||||||
|
return rsvp.SeeOther("/account", props)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = user.SetPassword(newPassword)
|
||||||
|
if err != nil {
|
||||||
|
props.GeneralError = "Something went wrong."
|
||||||
|
log.Printf("Set password failed: %s\n", err)
|
||||||
|
session.FlashSet(&props)
|
||||||
|
return rsvp.SeeOther("/account", props)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.RemoveValue("sessionKey")
|
||||||
|
|
||||||
|
session.FlashSet(&api.LoginProps{SuccessfulSetPassword: true})
|
||||||
|
return rsvp.SeeOther("/", "Set password successful!")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
lishwist "lishwist/core"
|
||||||
|
"lishwist/http/env"
|
||||||
|
"lishwist/http/response"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Teajey/rsvp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthProps struct {
|
||||||
|
GitVersion string
|
||||||
|
Config env.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func Health(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
|
if app.Admin() == nil {
|
||||||
|
return rsvp.Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsvp.Response{Body: HealthProps{
|
||||||
|
GitVersion: env.GitVersion,
|
||||||
|
Config: env.Configuration,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/Teajey/rsvp"
|
"github.com/Teajey/rsvp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExpectAppSession(next func(*lishwist.Session, http.Header, *http.Request) rsvp.Response) response.HandlerFunc {
|
func ExpectAppSession(next func(*lishwist.Session, *response.Session, http.Header, *http.Request) rsvp.Response) response.HandlerFunc {
|
||||||
return func(session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
return func(session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
sessionKey, ok := session.GetValue("sessionKey").(string)
|
sessionKey, ok := session.GetValue("sessionKey").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -29,6 +29,6 @@ func ExpectAppSession(next func(*lishwist.Session, http.Header, *http.Request) r
|
||||||
return response.Error(http.StatusInternalServerError, "Something went wrong.")
|
return response.Error(http.StatusInternalServerError, "Something went wrong.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(appSession, h, r)
|
return next(appSession, session, h, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ type foreignWishlistProps struct {
|
||||||
Gifts []lishwist.Wish
|
Gifts []lishwist.Wish
|
||||||
}
|
}
|
||||||
|
|
||||||
func ForeignWishlist(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func ForeignWishlist(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
userReference := r.PathValue("userReference")
|
userReference := r.PathValue("userReference")
|
||||||
user := app.User()
|
user := app.User()
|
||||||
if user.Reference == userReference {
|
if user.Reference == userReference {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func AdminGroup(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Resp
|
||||||
return response.Data("group_page.gotmpl", p)
|
return response.Data("group_page.gotmpl", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Group(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func Group(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
user := app.User()
|
user := app.User()
|
||||||
if user.IsAdmin {
|
if user.IsAdmin {
|
||||||
return AdminGroup(app, h, r)
|
return AdminGroup(app, h, r)
|
||||||
|
|
@ -73,7 +73,7 @@ func PublicGroup(s *response.Session, h http.Header, r *http.Request) rsvp.Respo
|
||||||
return response.Data("public_group_page.gotmpl", p)
|
return response.Data("public_group_page.gotmpl", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GroupPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func GroupPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
admin := app.Admin()
|
admin := app.Admin()
|
||||||
if admin == nil {
|
if admin == nil {
|
||||||
return response.NotFound()
|
return response.NotFound()
|
||||||
|
|
@ -138,7 +138,7 @@ func GroupPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Respo
|
||||||
return response.Data("", group)
|
return response.Data("", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Groups(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func Groups(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
admin := app.Admin()
|
admin := app.Admin()
|
||||||
if admin == nil {
|
if admin == nil {
|
||||||
return response.NotFound()
|
return response.NotFound()
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HomeProps struct {
|
type HomeProps struct {
|
||||||
Username string
|
Username string
|
||||||
Gifts []lishwist.Wish
|
Gifts []lishwist.Wish
|
||||||
Todo []lishwist.Wish
|
Todo []lishwist.Wish
|
||||||
Reference string
|
Reference string
|
||||||
HostUrl string
|
HostUrl string
|
||||||
Groups []lishwist.Group
|
Groups []lishwist.Group
|
||||||
|
AccountAlert bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func Home(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func Home(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
gifts, err := app.GetWishes()
|
gifts, err := app.GetWishes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get gifts: %s\n", err)
|
log.Printf("Failed to get gifts: %s\n", err)
|
||||||
|
|
@ -37,11 +38,11 @@ func Home(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
log.Printf("Failed to get groups: %s\n", err)
|
log.Printf("Failed to get groups: %s\n", err)
|
||||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(")
|
return response.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(")
|
||||||
}
|
}
|
||||||
p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String(), Groups: groups}
|
p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.Configuration.HostUrl, Groups: groups, AccountAlert: user.PasswordFromAdmin}
|
||||||
return response.Data("home.gotmpl", p)
|
return response.Data("home.gotmpl", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomePost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func HomePost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,9 @@ func Login(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
props.GeneralError = flashProps.GeneralError
|
props.GeneralError = flashProps.GeneralError
|
||||||
props.Username.Error = flashProps.Username.Error
|
props.Username.Error = flashProps.Username.Error
|
||||||
props.Password.Error = flashProps.Password.Error
|
props.Password.Error = flashProps.Password.Error
|
||||||
}
|
|
||||||
|
|
||||||
flash = s.FlashGet()
|
props.SuccessfulRegistration = flashProps.SuccessfulRegistration
|
||||||
successfulReg, _ := flash.(bool)
|
props.SuccessfulSetPassword = flashProps.SuccessfulSetPassword
|
||||||
if successfulReg {
|
|
||||||
props.SuccessfulRegistration = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rsvp.Response{TemplateName: "login.gotmpl", Body: props}
|
return rsvp.Response{TemplateName: "login.gotmpl", Body: props}
|
||||||
|
|
@ -61,7 +58,7 @@ func LoginPost(session *response.Session, h http.Header, r *http.Request) rsvp.R
|
||||||
var targ lishwist.ErrorInvalidCredentials
|
var targ lishwist.ErrorInvalidCredentials
|
||||||
switch {
|
switch {
|
||||||
case errors.As(err, &targ):
|
case errors.As(err, &targ):
|
||||||
props.GeneralError = "Username or password invalid"
|
props.GeneralError = "Username or password invalid. If you're having trouble accessing your account, you may want to consider asking the System Admin (Thomas) to reset your password"
|
||||||
session.FlashSet(&props)
|
session.FlashSet(&props)
|
||||||
log.Printf("Invalid credentials: %s: %#v\n", err, props)
|
log.Printf("Invalid credentials: %s: %#v\n", err, props)
|
||||||
return resp
|
return resp
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,6 @@ func RegisterPost(s *response.Session, h http.Header, r *http.Request) rsvp.Resp
|
||||||
return rsvp.SeeOther(r.URL.Path, props)
|
return rsvp.SeeOther(r.URL.Path, props)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.FlashSet(true)
|
s.FlashSet(&api.LoginProps{SuccessfulRegistration: true})
|
||||||
return rsvp.SeeOther("/", "Registration successful!")
|
return rsvp.SeeOther("/", "Registration successful!")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/Teajey/rsvp"
|
"github.com/Teajey/rsvp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Users(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func Users(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
admin := app.Admin()
|
admin := app.Admin()
|
||||||
if admin == nil {
|
if admin == nil {
|
||||||
return response.NotFound()
|
return response.NotFound()
|
||||||
|
|
@ -22,7 +22,7 @@ func Users(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response
|
||||||
return response.Data("", users)
|
return response.Data("", users)
|
||||||
}
|
}
|
||||||
|
|
||||||
func User(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func User(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
admin := app.Admin()
|
admin := app.Admin()
|
||||||
if admin == nil {
|
if admin == nil {
|
||||||
return response.NotFound()
|
return response.NotFound()
|
||||||
|
|
@ -41,7 +41,7 @@ func User(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
return response.Data("", user)
|
return response.Data("", user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func UserPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
admin := app.Admin()
|
admin := app.Admin()
|
||||||
if admin == nil {
|
if admin == nil {
|
||||||
return response.NotFound()
|
return response.NotFound()
|
||||||
|
|
@ -53,23 +53,33 @@ func UserPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Respon
|
||||||
}
|
}
|
||||||
|
|
||||||
reference := r.PathValue("userReference")
|
reference := r.PathValue("userReference")
|
||||||
if reference == app.User().Reference {
|
|
||||||
return response.Error(http.StatusForbidden, "You cannot delete yourself.")
|
|
||||||
}
|
|
||||||
|
|
||||||
intent := r.Form.Get("intent")
|
intent := r.Form.Get("intent")
|
||||||
|
|
||||||
switch intent {
|
if intent != "" {
|
||||||
case "delete":
|
switch intent {
|
||||||
err = admin.UserSetLive(reference, false)
|
case "delete":
|
||||||
if err != nil {
|
if reference == app.User().Reference {
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to delete user: %s", err)
|
return response.Error(http.StatusForbidden, "You cannot delete yourself.")
|
||||||
}
|
}
|
||||||
case "rename":
|
err = admin.UserSetLive(reference, false)
|
||||||
name := r.Form.Get("display_name")
|
if err != nil {
|
||||||
err = admin.RenameUser(reference, name)
|
return response.Error(http.StatusInternalServerError, "Failed to delete user: %s", err)
|
||||||
if err != nil {
|
}
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to rename user: %s", err)
|
case "rename":
|
||||||
|
name := r.Form.Get("display_name")
|
||||||
|
err = admin.RenameUser(reference, name)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(http.StatusInternalServerError, "Failed to rename user: %s", err)
|
||||||
|
}
|
||||||
|
case "set_password":
|
||||||
|
newPassword := r.Form.Get("new_password")
|
||||||
|
err = admin.SetUserPassword(reference, newPassword)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(http.StatusInternalServerError, "Failed to set new password: %s", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return response.Error(http.StatusBadRequest, "Invalid intent %q", intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func WishlistDelete(app *lishwist.Session, h http.Header, r *http.Request) rsvp.
|
||||||
return rsvp.SeeOther("/", "Wish deleted")
|
return rsvp.SeeOther("/", "Wish deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ForeignWishlistPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
func ForeignWishlistPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ func prefixPermanentRedirect(before, after string) response.HandlerFunc {
|
||||||
func Create(useSecureCookies bool) *router.VisibilityRouter {
|
func Create(useSecureCookies bool) *router.VisibilityRouter {
|
||||||
gob.Register(&api.RegisterProps{})
|
gob.Register(&api.RegisterProps{})
|
||||||
gob.Register(&api.LoginProps{})
|
gob.Register(&api.LoginProps{})
|
||||||
|
gob.Register(&routing.AccountProps{})
|
||||||
|
|
||||||
store := session.NewInMemoryStore([]byte(env.SessionSecret))
|
store := session.NewInMemoryStore([]byte(env.Configuration.SessionSecret))
|
||||||
store.Options.MaxAge = 86_400 // 24 hours in seconds
|
store.Options.MaxAge = 86_400 // 24 hours in seconds
|
||||||
store.Options.Secure = useSecureCookies
|
store.Options.Secure = useSecureCookies
|
||||||
store.Options.HttpOnly = true
|
store.Options.HttpOnly = true
|
||||||
|
|
@ -49,6 +50,8 @@ func Create(useSecureCookies bool) *router.VisibilityRouter {
|
||||||
r.Public.HandleFunc("POST /", routing.LoginPost)
|
r.Public.HandleFunc("POST /", routing.LoginPost)
|
||||||
r.Public.HandleFunc("POST /register", routing.RegisterPost)
|
r.Public.HandleFunc("POST /register", routing.RegisterPost)
|
||||||
|
|
||||||
|
r.Private.HandleFunc("GET /account", routing.ExpectAppSession(routing.Account))
|
||||||
|
r.Private.HandleFunc("GET /health", routing.ExpectAppSession(routing.Health))
|
||||||
r.Private.HandleFunc("GET /", routing.NotFound)
|
r.Private.HandleFunc("GET /", routing.NotFound)
|
||||||
r.Private.HandleFunc("GET /groups", routing.ExpectAppSession(routing.Groups))
|
r.Private.HandleFunc("GET /groups", routing.ExpectAppSession(routing.Groups))
|
||||||
r.Private.HandleFunc("GET /groups/{groupReference}", routing.ExpectAppSession(routing.Group))
|
r.Private.HandleFunc("GET /groups/{groupReference}", routing.ExpectAppSession(routing.Group))
|
||||||
|
|
@ -56,6 +59,7 @@ func Create(useSecureCookies bool) *router.VisibilityRouter {
|
||||||
r.Private.HandleFunc("GET /users", routing.ExpectAppSession(routing.Users))
|
r.Private.HandleFunc("GET /users", routing.ExpectAppSession(routing.Users))
|
||||||
r.Private.HandleFunc("GET /users/{userReference}", routing.ExpectAppSession(routing.User))
|
r.Private.HandleFunc("GET /users/{userReference}", routing.ExpectAppSession(routing.User))
|
||||||
r.Private.HandleFunc("GET /{$}", routing.ExpectAppSession(routing.Home))
|
r.Private.HandleFunc("GET /{$}", routing.ExpectAppSession(routing.Home))
|
||||||
|
r.Private.HandleFunc("POST /account", routing.ExpectAppSession(routing.AccountPost))
|
||||||
r.Private.HandleFunc("POST /groups/{groupReference}", routing.ExpectAppSession(routing.GroupPost))
|
r.Private.HandleFunc("POST /groups/{groupReference}", routing.ExpectAppSession(routing.GroupPost))
|
||||||
r.Private.HandleFunc("POST /lists/{userReference}", routing.ExpectAppSession(routing.ForeignWishlistPost))
|
r.Private.HandleFunc("POST /lists/{userReference}", routing.ExpectAppSession(routing.ForeignWishlistPost))
|
||||||
r.Private.HandleFunc("POST /logout", routing.LogoutPost)
|
r.Private.HandleFunc("POST /logout", routing.LogoutPost)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
{{template "head" .}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div style="height: 100svh;" class="d-flex flex-column">
|
||||||
|
<div class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="navbar-brand">Lishwist</div>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarToggle"
|
||||||
|
aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarToggle">
|
||||||
|
<nav>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="flex-grow-1"></div>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Logged in as '{{.CurrentUsername}}'
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<form class="d-contents" method="post" action="/logout">
|
||||||
|
<button class="dropdown-item" type="submit">Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-scroll flex-grow-1">
|
||||||
|
<div class="container py-5">
|
||||||
|
{{with .GeneralError}}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<p class="mb-0">{{.}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<section class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2>Submit new password</h2>
|
||||||
|
<div class="form-text">You can set a new password by submitting this form.</div>
|
||||||
|
{{with .PasswordFromAdmin}}
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<p class="mb-0"><span class="badge text-bg-danger">!</span> This is recommended, because your password has
|
||||||
|
been set by the admin. Change it to
|
||||||
|
something they don't know!</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form method="post">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<label>
|
||||||
|
New Password
|
||||||
|
{{template "input" .Password}}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Confirm password
|
||||||
|
{{template "input" .ConfirmPassword}}
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-primary" type="submit" name="intent" value="set_password">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -1,130 +1,138 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
{{template "head" .}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<head>
|
||||||
<div style="height: 100svh;" class="d-flex flex-column">
|
{{template "head" .}}
|
||||||
<div class="navbar navbar-expand-lg bg-body-tertiary">
|
</head>
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="navbar-brand">Lishwist</div>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarToggle"
|
|
||||||
aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarToggle">
|
|
||||||
<div class="flex-grow-1"></div>
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="nav-item"><button class="btn btn-success"
|
|
||||||
onclick="navigator.clipboard.writeText('{{.HostUrl}}/lists/{{.Reference}}'); alert('The share link to your wishlist has been copied to your clipboard. Anyone with the link will be able to claim gifts for you. Share it with someone!');">Copy
|
|
||||||
share link</button></li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
Logged in as '{{.Username}}'
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<form class="d-contents" method="post" action="/logout">
|
|
||||||
<button class="dropdown-item" type="submit">Logout</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-scroll flex-grow-1">
|
|
||||||
<div class="container py-5">
|
|
||||||
<section class="card mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2>Your wishlist</h2>
|
|
||||||
{{with .Gifts}}
|
|
||||||
<form method="post" onchange="acceptNames(this, 'deleteSubmit', 'gift')" autocomplete="off">
|
|
||||||
<ul class="list-group mb-3">
|
|
||||||
{{range .}}
|
|
||||||
<li class="list-group-item">
|
|
||||||
<input id="wishlist_select_{{.Id}}" class="form-check-input" type="checkbox" name="gift" value="{{.Id}}">
|
|
||||||
<label class="form-check-label stretched-link" for="wishlist_select_{{.Id}}">
|
|
||||||
{{.Name}}
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
<button id="deleteSubmit" class="btn btn-danger mb-3" type="submit" name="intent" value="delete_idea"
|
|
||||||
disabled>Delete</button>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
<p>Your list is empty. Think of some things to add!</p>
|
|
||||||
{{end}}
|
|
||||||
<form method="post">
|
|
||||||
<div class="input-group">
|
|
||||||
<input class="form-control" name="gift_name" required placeholder="Write a gift idea here" autofocus>
|
|
||||||
<button class="btn btn-primary" type="submit" name="intent" value="add_idea">Add gift idea</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card mb-4">
|
<body>
|
||||||
<div class="card-body">
|
<div style="height: 100svh;" class="d-flex flex-column">
|
||||||
<h2>Your todo list</h2>
|
<div class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
{{with .Todo}}
|
<div class="container-fluid">
|
||||||
<form method="post"
|
<div class="navbar-brand">Lishwist</div>
|
||||||
onchange="acceptNames(this, 'unclaimSubmit', 'gift'); acceptNames(this, 'completeSubmit', 'gift')"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarToggle"
|
||||||
autocomplete="off">
|
aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<ul class="list-group mb-3">
|
<span class="navbar-toggler-icon"></span>{{if .AccountAlert}} <span
|
||||||
{{range .}}
|
class="badge text-bg-danger">!</span>{{end}}
|
||||||
<li class="list-group-item{{if .Sent}} list-group-item-light{{end}}">
|
</button>
|
||||||
<input id="todo_select_{{.Id}}" class="form-check-input" type="checkbox" {{if .Sent}}
|
<div class="collapse navbar-collapse" id="navbarToggle">
|
||||||
aria-describedby="todo_detail_{{.Id}}" disabled{{else}} name="gift" value="{{.Id}}" {{end}}>
|
<div class="flex-grow-1"></div>
|
||||||
<label for="todo_select_{{.Id}}" class="form-check-label">
|
<ul class="navbar-nav">
|
||||||
<em>
|
<li class="nav-item"><button class="btn btn-success"
|
||||||
{{if .Sent}}
|
onclick="navigator.clipboard.writeText('{{.HostUrl}}/lists/{{.Reference}}'); alert('The share link to your wishlist has been copied to your clipboard. Anyone with the link will be able to claim gifts for you. Share it with someone!');">Copy
|
||||||
<s>{{.Name}}</s>
|
share link</button></li>
|
||||||
{{else}}
|
<li class="nav-item">
|
||||||
{{.Name}}
|
<div class="dropdown">
|
||||||
{{end}}
|
<button class="nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
</em>
|
Logged in as '{{.Username}}'{{if .AccountAlert}} <span class="badge text-bg-danger">!</span>{{end}}
|
||||||
</label>
|
</button>
|
||||||
<span id="todo_detail_{{.Id}}">
|
<ul class="dropdown-menu">
|
||||||
for <a href="/lists/{{.RecipientRef}}">{{.RecipientName}}</a>
|
<li>
|
||||||
</span>
|
<a class="dropdown-item" href="/account">Account{{if .AccountAlert}} <span
|
||||||
|
class="badge text-bg-danger">!</span>{{end}}</a>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
<li>
|
||||||
</ul>
|
<form class="d-contents" method="post" action="/logout">
|
||||||
<button id="unclaimSubmit" class="btn btn-warning" type="submit" name="intent" value="unclaim_todo"
|
<button class="dropdown-item" type="submit">Logout</button>
|
||||||
disabled>Unclaim</button>
|
</form>
|
||||||
<button id="completeSubmit" class="btn btn-success" type="submit" name="intent" value="complete_todo"
|
|
||||||
disabled>Complete</button>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
<p class="mb-0">When you claim gifts for others, they will appear here.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2>Your groups</h2>
|
|
||||||
{{with .Groups}}
|
|
||||||
<ul class="list-group">
|
|
||||||
{{range .}}
|
|
||||||
<li class="list-group-item">
|
|
||||||
<a href="/groups/{{.Reference}}">{{.Name}}</a>
|
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
</div>
|
||||||
<p>You don't belong to any groups</p>
|
</li>
|
||||||
{{end}}
|
</ul>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
<div class="overflow-y-scroll flex-grow-1">
|
||||||
</html>
|
<div class="container py-5">
|
||||||
|
<section class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2>Your wishlist</h2>
|
||||||
|
{{with .Gifts}}
|
||||||
|
<form method="post" onchange="acceptNames(this, 'deleteSubmit', 'gift')" autocomplete="off">
|
||||||
|
<ul class="list-group mb-3">
|
||||||
|
{{range .}}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<input id="wishlist_select_{{.Id}}" class="form-check-input" type="checkbox" name="gift"
|
||||||
|
value="{{.Id}}">
|
||||||
|
<label class="form-check-label stretched-link" for="wishlist_select_{{.Id}}">
|
||||||
|
{{.Name}}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<button id="deleteSubmit" class="btn btn-danger mb-3" type="submit" name="intent" value="delete_idea"
|
||||||
|
disabled>Delete</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<p>Your list is empty. Think of some things to add!</p>
|
||||||
|
{{end}}
|
||||||
|
<form method="post">
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" name="gift_name" required placeholder="Write a gift idea here" autofocus>
|
||||||
|
<button class="btn btn-primary" type="submit" name="intent" value="add_idea">Add gift idea</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2>Your todo list</h2>
|
||||||
|
{{with .Todo}}
|
||||||
|
<form method="post"
|
||||||
|
onchange="acceptNames(this, 'unclaimSubmit', 'gift'); acceptNames(this, 'completeSubmit', 'gift')"
|
||||||
|
autocomplete="off">
|
||||||
|
<ul class="list-group mb-3">
|
||||||
|
{{range .}}
|
||||||
|
<li class="list-group-item{{if .Sent}} list-group-item-light{{end}}">
|
||||||
|
<input id="todo_select_{{.Id}}" class="form-check-input" type="checkbox" {{if .Sent}}
|
||||||
|
aria-describedby="todo_detail_{{.Id}}" disabled{{else}} name="gift" value="{{.Id}}" {{end}}>
|
||||||
|
<label for="todo_select_{{.Id}}" class="form-check-label">
|
||||||
|
<em>
|
||||||
|
{{if .Sent}}
|
||||||
|
<s>{{.Name}}</s>
|
||||||
|
{{else}}
|
||||||
|
{{.Name}}
|
||||||
|
{{end}}
|
||||||
|
</em>
|
||||||
|
</label>
|
||||||
|
<span id="todo_detail_{{.Id}}">
|
||||||
|
for <a href="/lists/{{.RecipientRef}}">{{.RecipientName}}</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<button id="unclaimSubmit" class="btn btn-warning" type="submit" name="intent" value="unclaim_todo"
|
||||||
|
disabled>Unclaim</button>
|
||||||
|
<button id="completeSubmit" class="btn btn-success" type="submit" name="intent" value="complete_todo"
|
||||||
|
disabled>Complete</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<p class="mb-0">When you claim gifts for others, they will appear here.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2>Your groups</h2>
|
||||||
|
{{with .Groups}}
|
||||||
|
<ul class="list-group">
|
||||||
|
{{range .}}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="/groups/{{.Reference}}">{{.Name}}</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
<p>You don't belong to any groups</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -23,6 +23,11 @@
|
||||||
<p class="mb-0">Registration successful. Now you can login.</p>
|
<p class="mb-0">Registration successful. Now you can login.</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .SuccessfulSetPassword}}
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
<p class="mb-0">Set password successfully. You can now login back in.</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{with .GeneralError}}
|
{{with .GeneralError}}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
<p class="mb-0">{{.}}</p>
|
<p class="mb-0">{{.}}</p>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
Just a normal login form. Takes the following form values:
|
||||||
|
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
|
||||||
|
Upon successful login, a redirect to the same URL on the protected router is issued.
|
||||||
|
|
||||||
|
A registration page is also available at /register
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
Register with the following form values:
|
||||||
|
|
||||||
|
username string
|
||||||
|
newPassword string
|
||||||
|
confirmPassword string
|
||||||
|
|
||||||
|
All must be provided, username must be unique, newPassword and confirmPassword must be identical.
|
||||||
|
|
||||||
|
It's worth considering that having two password form parameters isn't very helpful on the command line, it's only really useful to the browser.
|
||||||
|
Therefore, it might be a good idea to have Javascript check these two fields are identical, and submit, rather than having this
|
||||||
|
checked by the server. Meaning only one password field needs to be submitted to the server. That does block non-Javascript browsers tho :^P
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Template *template.Template
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Template = load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func load() *template.Template {
|
||||||
|
t := template.Must(template.ParseGlob("templates/text/*.gotmpl"))
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo $(git rev-parse HEAD)$(test -n "$(git status --porcelain)" && echo "*")
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
APISNAP=api.snap.txt
|
APISNAP=api.snap.txt
|
||||||
|
|
||||||
./scripts/api_snapshot lishwist/core > core/$APISNAP
|
./scripts/api_snapshot ./core > core/$APISNAP
|
||||||
git diff --quiet core/$APISNAP
|
git diff --quiet core/$APISNAP
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue