Compare commits

..

No commits in common. "3509cb9666822e26dc7bb410a9d2956a63044d6c" and "a2932d7b1c0fd00814186ca43a6d1798d1fa643b" have entirely different histories.

36 changed files with 224 additions and 573 deletions

View File

@ -1,9 +1,9 @@
package lishwist // import "."
package lishwist // import "lishwist/core"
VARIABLES
var ErrorUsernameTaken = errors.New("username is taken")
var ErrorUsernameTaken = errors.New("Username is taken")
FUNCTIONS
@ -31,8 +31,6 @@ func (a *Admin) RemoveUserFromGroup(userId, groupId 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
type ErrorInvalidCredentials error
@ -83,13 +81,12 @@ func (u *Session) SuggestWishForUser(otherUserReference string, wishName string)
func (s *Session) User() User
type User struct {
Id string
NormalName string
Name string
Reference string
IsAdmin bool
IsLive bool
PasswordFromAdmin bool
Id string
NormalName string
Name string
Reference string
IsAdmin bool
IsLive bool
}
func GetUserByReference(reference string) (*User, error)
@ -98,8 +95,6 @@ func Register(username, newPassword string) (*User, error)
func (u *User) GetTodo() ([]Wish, error)
func (u *User) SetPassword(newPassword string) error
func (u *User) WishCount() (int, error)
type Wish struct {

View File

@ -8,7 +8,6 @@ CREATE TABLE IF NOT EXISTS "user" (
"password_hash" TEXT NOT NULL,
"is_admin" INTEGER NOT NULL DEFAULT 0,
"is_live" INTEGER NOT NULL DEFAULT 1,
"password_from_admin" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "wish" (

View File

@ -7,7 +7,7 @@ import (
"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) {
if username == "" {
@ -24,17 +24,17 @@ func Register(username, newPassword string) (*User, error) {
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
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()
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)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
return nil, fmt.Errorf("Failed to create user: %w\n", err)
}
return user, nil

View File

@ -23,7 +23,7 @@ func (s *Session) User() User {
func SessionFromKey(key string) (*Session, error) {
s := Session{}
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 = ?"
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 = ?"
var expiry string
err := db.Connection.QueryRow(query, key).Scan(
&s.user.Id,
@ -32,7 +32,6 @@ func SessionFromKey(key string) (*Session, error) {
&s.user.Reference,
&s.user.IsAdmin,
&s.user.IsLive,
&s.user.PasswordFromAdmin,
&s.Key,
&expiry,
)

View File

@ -4,20 +4,19 @@ import (
"fmt"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"lishwist/core/internal/db"
"lishwist/core/internal/normalize"
)
type User struct {
Id string
NormalName string
Name string
Reference string
IsAdmin bool
IsLive bool
PasswordFromAdmin bool
Id string
NormalName string
// TODO: rename to DisplayName
Name string
Reference string
IsAdmin bool
IsLive bool
}
func queryManyUsers(query string, args ...any) ([]User, error) {
@ -29,7 +28,7 @@ func queryManyUsers(query string, args ...any) ([]User, error) {
users := []User{}
for rows.Next() {
var u User
err = rows.Scan(&u.Id, &u.NormalName, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive, &u.PasswordFromAdmin)
err = rows.Scan(&u.Id, &u.NormalName, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive)
if err != nil {
return nil, err
}
@ -55,7 +54,7 @@ func queryOneUser(query string, args ...any) (*User, error) {
func getUserByName(username string) (*User, error) {
username = normalize.Name(username)
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM v_user WHERE name = ?"
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE name = ?"
return queryOneUser(stmt, username)
}
@ -92,12 +91,12 @@ func (u *User) getPassHash() ([]byte, error) {
}
func getUserByReference(reference string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM v_user WHERE reference = ?"
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE reference = ?"
return queryOneUser(stmt, reference)
}
func getUserById(id string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM v_user WHERE id = ?"
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE id = ?"
return queryOneUser(stmt, id)
}
@ -112,7 +111,7 @@ func hasUsers() (bool, error) {
}
func (*Admin) ListUsers() ([]User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live, password_from_admin FROM user"
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM user"
return queryManyUsers(stmt)
}
@ -173,29 +172,3 @@ func (u *Admin) RenameUser(userReference string, displayName string) error {
}
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
}

View File

@ -7,7 +7,6 @@ import (
type LoginProps struct {
GeneralError string `json:",omitempty"`
SuccessfulRegistration bool `json:",omitempty"`
SuccessfulSetPassword bool `json:",omitempty"`
Username templates.InputProps
Password templates.InputProps
}

View File

@ -1,4 +0,0 @@
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

48
http/env/env.go vendored
View File

@ -14,34 +14,20 @@ func GuaranteeEnv(key string) string {
return variable
}
type Config struct {
DatabaseFile string
SessionSecret string
HostRootUrl string
HostPort string
ServePort string
InDev bool
HostUrl string
}
var Configuration Config
func init() {
Configuration.DatabaseFile = GuaranteeEnv("LISHWIST_DATABASE_FILE")
Configuration.SessionSecret = GuaranteeEnv("LISHWIST_SESSION_SECRET")
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()
}()
}
var DatabaseFile = GuaranteeEnv("LISHWIST_DATABASE_FILE")
var SessionSecret = GuaranteeEnv("LISHWIST_SESSION_SECRET")
var HostRootUrl = GuaranteeEnv("LISHWIST_HOST_ROOT_URL")
var HostPort = os.Getenv("LISHWIST_HOST_PORT")
var ServePort = GuaranteeEnv("LISHWIST_SERVE_PORT")
var InDev = os.Getenv("LISHWIST_IN_DEV") != ""
var HostUrl = func() *url.URL {
rawUrl := HostRootUrl
if HostPort != "" {
rawUrl += ":" + HostPort
}
u, err := url.Parse(rawUrl)
if err != nil {
log.Fatalln("Couldn't parse host url:", err)
}
return u
}()

3
http/env/version.go vendored
View File

@ -1,3 +0,0 @@
package env
var GitVersion string

View File

@ -10,16 +10,16 @@ import (
)
func main() {
err := lishwist.Init(env.Configuration.DatabaseFile)
err := lishwist.Init(env.DatabaseFile)
if err != nil {
log.Fatalf("Failed to init Lishwist: %s\n", err)
}
useSecureCookies := !env.Configuration.InDev
useSecureCookies := !env.InDev
r := server.Create(useSecureCookies)
log.Printf("Running at http://127.0.0.1:%s\n", env.Configuration.ServePort)
err = http.ListenAndServe(":"+env.Configuration.ServePort, r)
log.Printf("Running at http://127.0.0.1:%s\n", env.ServePort)
err = http.ListenAndServe(":"+env.ServePort, r)
if err != nil {
log.Fatalln("Failed to listen and server:", err)
}

View File

@ -6,7 +6,6 @@ import (
"lishwist/http/session"
"lishwist/http/templates"
"lishwist/http/templates/text"
"github.com/Teajey/rsvp"
)
@ -19,7 +18,6 @@ type ServeMux struct {
func NewServeMux(store *session.Store) *ServeMux {
mux := rsvp.NewServeMux()
mux.Config.HtmlTemplate = templates.Template
mux.Config.TextTemplate = text.Template
return &ServeMux{
inner: mux,
store: store,

View File

@ -9,28 +9,31 @@ type Session struct {
written bool
}
const flashKey = "_flash"
func (s *Session) FlashGet() any {
val, ok := s.inner.Values[flashKey]
if !ok {
list := s.inner.Flashes()
if len(list) < 1 {
return nil
} else {
s.written = true
return list[0]
}
delete(s.inner.Values, flashKey)
s.written = true
return val
}
func (s *Session) FlashPeek() any {
val, ok := s.inner.Values[flashKey]
flash, ok := s.inner.Values["_flash"]
if !ok {
return nil
}
return val
list := flash.([]any)
if len(list) < 1 {
return nil
} else {
return list[0]
}
}
func (s *Session) FlashSet(value any) {
s.inner.Values[flashKey] = value
s.inner.AddFlash(value)
s.written = true
}
@ -44,17 +47,12 @@ func (s *Session) SetValue(key any, value any) {
s.written = true
}
func (s *Session) RemoveValue(key any) {
delete(s.inner.Values, key)
s.written = true
}
func (s *Session) GetValue(key any) any {
return s.inner.Values[key]
}
func (s *Session) ClearValues() {
s.inner.Values = make(map[any]any)
s.inner.Values = nil
s.written = true
}

View File

@ -1,116 +0,0 @@
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!")
}

View File

@ -1,26 +0,0 @@
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,
}}
}

View File

@ -11,7 +11,7 @@ import (
"github.com/Teajey/rsvp"
)
func ExpectAppSession(next func(*lishwist.Session, *response.Session, http.Header, *http.Request) rsvp.Response) response.HandlerFunc {
func ExpectAppSession(next func(*lishwist.Session, http.Header, *http.Request) rsvp.Response) response.HandlerFunc {
return func(session *response.Session, h http.Header, r *http.Request) rsvp.Response {
sessionKey, ok := session.GetValue("sessionKey").(string)
if !ok {
@ -29,6 +29,6 @@ func ExpectAppSession(next func(*lishwist.Session, *response.Session, http.Heade
return response.Error(http.StatusInternalServerError, "Something went wrong.")
}
return next(appSession, session, h, r)
return next(appSession, h, r)
}
}

View File

@ -16,7 +16,7 @@ type foreignWishlistProps struct {
Gifts []lishwist.Wish
}
func ForeignWishlist(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func ForeignWishlist(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
userReference := r.PathValue("userReference")
user := app.User()
if user.Reference == userReference {

View File

@ -37,7 +37,7 @@ func AdminGroup(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Resp
return response.Data("group_page.gotmpl", p)
}
func Group(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func Group(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
user := app.User()
if user.IsAdmin {
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)
}
func GroupPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func GroupPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return response.NotFound()
@ -138,7 +138,7 @@ func GroupPost(app *lishwist.Session, session *response.Session, h http.Header,
return response.Data("", group)
}
func Groups(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func Groups(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return response.NotFound()

View File

@ -12,16 +12,15 @@ import (
)
type HomeProps struct {
Username string
Gifts []lishwist.Wish
Todo []lishwist.Wish
Reference string
HostUrl string
Groups []lishwist.Group
AccountAlert bool
Username string
Gifts []lishwist.Wish
Todo []lishwist.Wish
Reference string
HostUrl string
Groups []lishwist.Group
}
func Home(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func Home(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
gifts, err := app.GetWishes()
if err != nil {
log.Printf("Failed to get gifts: %s\n", err)
@ -38,11 +37,11 @@ func Home(app *lishwist.Session, session *response.Session, h http.Header, r *ht
log.Printf("Failed to get groups: %s\n", err)
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.Configuration.HostUrl, Groups: groups, AccountAlert: user.PasswordFromAdmin}
p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String(), Groups: groups}
return response.Data("home.gotmpl", p)
}
func HomePost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func HomePost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
err := r.ParseForm()
if err != nil {
return response.Error(http.StatusBadRequest, "Failed to parse form")

View File

@ -24,9 +24,12 @@ func Login(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
props.GeneralError = flashProps.GeneralError
props.Username.Error = flashProps.Username.Error
props.Password.Error = flashProps.Password.Error
}
props.SuccessfulRegistration = flashProps.SuccessfulRegistration
props.SuccessfulSetPassword = flashProps.SuccessfulSetPassword
flash = s.FlashGet()
successfulReg, _ := flash.(bool)
if successfulReg {
props.SuccessfulRegistration = true
}
return rsvp.Response{TemplateName: "login.gotmpl", Body: props}
@ -58,7 +61,7 @@ func LoginPost(session *response.Session, h http.Header, r *http.Request) rsvp.R
var targ lishwist.ErrorInvalidCredentials
switch {
case errors.As(err, &targ):
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"
props.GeneralError = "Username or password invalid"
session.FlashSet(&props)
log.Printf("Invalid credentials: %s: %#v\n", err, props)
return resp

View File

@ -61,6 +61,6 @@ func RegisterPost(s *response.Session, h http.Header, r *http.Request) rsvp.Resp
return rsvp.SeeOther(r.URL.Path, props)
}
s.FlashSet(&api.LoginProps{SuccessfulRegistration: true})
s.FlashSet(true)
return rsvp.SeeOther("/", "Registration successful!")
}

View File

@ -8,7 +8,7 @@ import (
"github.com/Teajey/rsvp"
)
func Users(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func Users(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return response.NotFound()
@ -22,7 +22,7 @@ func Users(app *lishwist.Session, session *response.Session, h http.Header, r *h
return response.Data("", users)
}
func User(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func User(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return response.NotFound()
@ -41,7 +41,7 @@ func User(app *lishwist.Session, session *response.Session, h http.Header, r *ht
return response.Data("", user)
}
func UserPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func UserPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return response.NotFound()
@ -53,33 +53,23 @@ func UserPost(app *lishwist.Session, session *response.Session, h http.Header, r
}
reference := r.PathValue("userReference")
if reference == app.User().Reference {
return response.Error(http.StatusForbidden, "You cannot delete yourself.")
}
intent := r.Form.Get("intent")
if intent != "" {
switch intent {
case "delete":
if reference == app.User().Reference {
return response.Error(http.StatusForbidden, "You cannot delete yourself.")
}
err = admin.UserSetLive(reference, false)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete 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)
switch intent {
case "delete":
err = admin.UserSetLive(reference, false)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete 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)
}
}

View File

@ -40,7 +40,7 @@ func WishlistDelete(app *lishwist.Session, h http.Header, r *http.Request) rsvp.
return rsvp.SeeOther("/", "Wish deleted")
}
func ForeignWishlistPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
func ForeignWishlistPost(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
err := r.ParseForm()
if err != nil {
return response.Error(http.StatusBadRequest, "Failed to parse form")

View File

@ -32,9 +32,8 @@ func prefixPermanentRedirect(before, after string) response.HandlerFunc {
func Create(useSecureCookies bool) *router.VisibilityRouter {
gob.Register(&api.RegisterProps{})
gob.Register(&api.LoginProps{})
gob.Register(&routing.AccountProps{})
store := session.NewInMemoryStore([]byte(env.Configuration.SessionSecret))
store := session.NewInMemoryStore([]byte(env.SessionSecret))
store.Options.MaxAge = 86_400 // 24 hours in seconds
store.Options.Secure = useSecureCookies
store.Options.HttpOnly = true
@ -50,8 +49,6 @@ func Create(useSecureCookies bool) *router.VisibilityRouter {
r.Public.HandleFunc("POST /", routing.LoginPost)
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 /groups", routing.ExpectAppSession(routing.Groups))
r.Private.HandleFunc("GET /groups/{groupReference}", routing.ExpectAppSession(routing.Group))
@ -59,7 +56,6 @@ func Create(useSecureCookies bool) *router.VisibilityRouter {
r.Private.HandleFunc("GET /users", routing.ExpectAppSession(routing.Users))
r.Private.HandleFunc("GET /users/{userReference}", routing.ExpectAppSession(routing.User))
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 /lists/{userReference}", routing.ExpectAppSession(routing.ForeignWishlistPost))
r.Private.HandleFunc("POST /logout", routing.LogoutPost)

View File

@ -1,83 +0,0 @@
<!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>

View File

@ -1,138 +1,130 @@
<!doctype html>
<html>
<head>
{{template "head" .}}
</head>
<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>{{if .AccountAlert}} <span
class="badge text-bg-danger">!</span>{{end}}
</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}}'{{if .AccountAlert}} <span class="badge text-bg-danger">!</span>{{end}}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/account">Account{{if .AccountAlert}} <span
class="badge text-bg-danger">!</span>{{end}}</a>
</li>
<li>
<form class="d-contents" method="post" action="/logout">
<button class="dropdown-item" type="submit">Logout</button>
</form>
<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">
<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>
</div>
</li>
</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>
<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">
<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>
</body>
</html>

View File

@ -23,11 +23,6 @@
<p class="mb-0">Registration successful. Now you can login.</p>
</div>
{{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}}
<div class="alert alert-danger" role="alert">
<p class="mb-0">{{.}}</p>

View File

@ -1,8 +0,0 @@
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

View File

@ -1,11 +0,0 @@
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

View File

@ -1,17 +0,0 @@
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
}

View File

@ -1,3 +0,0 @@
#!/bin/sh
echo $(git rev-parse HEAD)$(test -n "$(git status --porcelain)" && echo "*")

View File

@ -2,7 +2,7 @@
APISNAP=api.snap.txt
./scripts/api_snapshot ./core > core/$APISNAP
./scripts/api_snapshot lishwist/core > core/$APISNAP
git diff --quiet core/$APISNAP
if [[ $? -ne 0 ]]; then