Compare commits
46 Commits
post-core-
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
86c1e4fea3 | |
|
|
192da1a74f | |
|
|
0d0aeffef1 | |
|
|
7bd73b4ddc | |
|
|
b9d1d1c1e1 | |
|
|
2291a338e6 | |
|
|
609ccfc0c4 | |
|
|
d6de403880 | |
|
|
0376cecb82 | |
|
|
a2cd38617a | |
|
|
1ad2b3e097 | |
|
|
9c9e0ebaff | |
|
|
c997908d3c | |
|
|
fd2cd3b358 | |
|
|
3509cb9666 | |
|
|
f110283b8e | |
|
|
1980e33d0f | |
|
|
55e6be7239 | |
|
|
b57652e6d2 | |
|
|
3cfeca65fc | |
|
|
a2932d7b1c | |
|
|
69b7f9717a | |
|
|
cc7c0d0834 | |
|
|
24bc67a8e2 | |
|
|
af13dc4558 | |
|
|
4783ee7fe9 | |
|
|
a4b166e4f0 | |
|
|
189719ec02 | |
|
|
24697f40dd | |
|
|
73a91be228 | |
|
|
7147a4378d | |
|
|
92ad5f5e90 | |
|
|
935d6c7a28 | |
|
|
cd41c55c02 | |
|
|
cffeede0dc | |
|
|
d909adb6fa | |
|
|
a1ac719229 | |
|
|
98853e4efd | |
|
|
eae0a7e0e3 | |
|
|
abb9c54036 | |
|
|
57e18ae0ce | |
|
|
a826417745 | |
|
|
c763ff40d4 | |
|
|
dfa2525714 | |
|
|
d33c02a5ac | |
|
|
0603386f4c |
|
|
@ -5,7 +5,7 @@ type Admin struct {
|
|||
}
|
||||
|
||||
func (s *Session) Admin() *Admin {
|
||||
if s.User.IsAdmin {
|
||||
if s.User().IsAdmin {
|
||||
return &Admin{s}
|
||||
} else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package lishwist // import "."
|
||||
|
||||
|
||||
VARIABLES
|
||||
|
||||
var ErrorUsernameTaken = errors.New("username is taken")
|
||||
|
||||
FUNCTIONS
|
||||
|
||||
func Init(dataSourceName string) error
|
||||
func PrintTables(d *sql.DB)
|
||||
func PrintViews(d *sql.DB)
|
||||
|
||||
TYPES
|
||||
|
||||
type Admin struct {
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func (a *Admin) AddUserToGroup(userId, groupId string) error
|
||||
|
||||
func (a *Admin) CreateGroup(name string, reference string) (*Group, error)
|
||||
|
||||
func (*Admin) GetUser(id string) (*User, error)
|
||||
|
||||
func (a *Admin) ListEvents() ([]Event, error)
|
||||
|
||||
func (a *Admin) ListGroups() ([]Group, error)
|
||||
|
||||
func (*Admin) ListUsers() ([]User, error)
|
||||
|
||||
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
|
||||
|
||||
type Event struct {
|
||||
Id string
|
||||
ActorId string
|
||||
ActionType string
|
||||
TargetType string
|
||||
TargetId string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Id string
|
||||
Name string
|
||||
Reference string
|
||||
Members []User
|
||||
}
|
||||
|
||||
func GetGroupByReference(reference string) (*Group, error)
|
||||
|
||||
func (g *Group) MemberIndex(userId string) int
|
||||
|
||||
type Session struct {
|
||||
Key string
|
||||
Expiry time.Time
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func Login(username, password string, sessionMaxAge time.Duration) (*Session, error)
|
||||
|
||||
func SessionFromKey(key string) (*Session, error)
|
||||
|
||||
func (s *Session) Admin() *Admin
|
||||
|
||||
func (s *Session) ClaimWishes(claims, unclaims []string) error
|
||||
|
||||
func (s *Session) CompleteWishes(claims []string) error
|
||||
|
||||
func (s *Session) GetGroupByReference(reference string) (*Group, error)
|
||||
|
||||
func (u *Session) GetGroups() ([]Group, error)
|
||||
|
||||
func (s *Session) GetOthersWishes(userReference string) ([]Wish, error)
|
||||
|
||||
func (s *Session) GetWishes() ([]Wish, error)
|
||||
|
||||
func (s *Session) MakeWish(name string) (string, error)
|
||||
|
||||
func (s *Session) RecindWishesForUser(ids ...string) error
|
||||
|
||||
func (s *Session) RevokeWishes(ids ...string) error
|
||||
|
||||
func (u *Session) SuggestWishForUser(otherUserReference string, wishName string) error
|
||||
|
||||
func (s *Session) User() User
|
||||
|
||||
type User struct {
|
||||
Id string
|
||||
NormalName string
|
||||
Name string
|
||||
Reference string
|
||||
IsAdmin bool
|
||||
IsLive bool
|
||||
PasswordFromAdmin bool
|
||||
}
|
||||
|
||||
func GetUserByReference(reference string) (*User, error)
|
||||
|
||||
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 {
|
||||
Id string
|
||||
Name string
|
||||
ClaimantId string `json:",omitempty"`
|
||||
ClaimantName string `json:",omitempty"`
|
||||
Sent bool
|
||||
RecipientId string `json:",omitempty"`
|
||||
RecipientName string `json:",omitempty"`
|
||||
RecipientRef string `json:",omitempty"`
|
||||
CreatorId string `json:",omitempty"`
|
||||
CreatorName string `json:",omitempty"`
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package lishwist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lishwist/core/internal/db"
|
||||
)
|
||||
|
||||
const (
|
||||
eventActionCreate = "CREATE"
|
||||
eventActionHide = "HIDE"
|
||||
eventActionUnhide = "UNHIDE"
|
||||
eventActionClaim = "CLAIM"
|
||||
eventActionUnclaim = "UNCLAIM"
|
||||
eventActionComplete = "COMPLETE"
|
||||
// eventActionDelete = "DELETE" NOTE: We can't have this, because there'll be no target to reference
|
||||
)
|
||||
|
||||
const (
|
||||
eventTargetGroup = "GROUP"
|
||||
eventTargetUser = "USER"
|
||||
eventTargetWish = "WISH"
|
||||
eventTargetGroupMember = "GROUP_MEMBER"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Id string
|
||||
ActorId string
|
||||
ActionType string
|
||||
TargetType string
|
||||
TargetId string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// type EventCreateGroupMember struct {
|
||||
// Event
|
||||
// Actor User
|
||||
// User
|
||||
// Group
|
||||
// }
|
||||
|
||||
func queryManyEvents(query string, args ...any) ([]Event, error) {
|
||||
rows, err := db.Connection.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
events := []Event{}
|
||||
for rows.Next() {
|
||||
var g Event
|
||||
var createdAt string
|
||||
err = rows.Scan(&g.Id, &g.ActorId, &g.ActionType, &g.TargetType, &g.TargetId, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.CreatedAt, err = time.Parse(time.RFC3339Nano, createdAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse created_at: %w", err)
|
||||
}
|
||||
events = append(events, g)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func queryOneEvent(query string, args ...any) (*Event, error) {
|
||||
events, err := queryManyEvents(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(events) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
return &events[0], nil
|
||||
}
|
||||
|
||||
func (a *Admin) ListEvents() ([]Event, error) {
|
||||
query := "SELECT id, actor_id, action_type, target_type, target_id, created_at FROM event;"
|
||||
return queryManyEvents(query)
|
||||
}
|
||||
|
||||
func recordEvent(actorId, actionType, targetType string, targetIds ...string) {
|
||||
// TODO: If this were to accept sql.Tx it could be used in atomic transactions
|
||||
numTargets := len(targetIds)
|
||||
if numTargets < 1 {
|
||||
log.Println("Warning: recordEvent called with no target IDs. Skipping.")
|
||||
return
|
||||
}
|
||||
stmt := "INSERT INTO event (actor_id, action_type, target_type, target_id) VALUES (?, ?, ?, ?)"
|
||||
extraValuePlaceholders := strings.Repeat(", (?, ?, ?, ?)", numTargets-1)
|
||||
args := make([]any, numTargets*4)
|
||||
for i, id := range targetIds {
|
||||
args[i*4] = actorId
|
||||
args[i*4+1] = actionType
|
||||
args[i*4+2] = targetType
|
||||
args[i*4+3] = id
|
||||
}
|
||||
_, err := db.Connection.Exec(stmt+extraValuePlaceholders, args...)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if numTargets == 1 {
|
||||
log.Printf("Failed to record %s %s event: failed to execute query: %s\n", actionType, targetType, err)
|
||||
} else {
|
||||
log.Printf("Failed to record %d %s %s events: failed to execute query: %s\n", numTargets, actionType, targetType, err)
|
||||
}
|
||||
}
|
||||
|
||||
func recordEventCreateGroup(actorId, groupId string) {
|
||||
recordEvent(actorId, eventActionCreate, eventTargetGroup, groupId)
|
||||
}
|
||||
|
||||
func recordEventCreateUser(actorId, userId string) {
|
||||
recordEvent(actorId, eventActionCreate, eventTargetUser, userId)
|
||||
}
|
||||
|
||||
func recordEventCreateWish(actorId, wishId string) {
|
||||
recordEvent(actorId, eventActionCreate, eventTargetWish, wishId)
|
||||
}
|
||||
|
||||
func recordEventCreateGroupMember(actorId, groupMemberId string) {
|
||||
recordEvent(actorId, eventActionCreate, eventTargetGroupMember, groupMemberId)
|
||||
}
|
||||
|
||||
// FIXME: I can't use these yet because the associated actions use reference
|
||||
// func recordEventHideUser(actorId, userId string) {
|
||||
// recordEvent(actorId, eventActionHide, eventTargetUser, userId)
|
||||
// }
|
||||
|
||||
// func recordEventUnhideUser(actorId, userId string) {
|
||||
// recordEvent(actorId, eventActionUnhide, eventTargetUser, userId)
|
||||
// }
|
||||
|
||||
func recordEventClaimWishes(actorId string, wishIds ...string) {
|
||||
recordEvent(actorId, eventActionClaim, eventTargetWish, wishIds...)
|
||||
}
|
||||
|
||||
func recordEventUnclaimWishes(actorId string, wishIds ...string) {
|
||||
recordEvent(actorId, eventActionUnclaim, eventTargetWish, wishIds...)
|
||||
}
|
||||
|
||||
func recordEventCompleteWishes(actorId string, wishIds ...string) {
|
||||
recordEvent(actorId, eventActionComplete, eventTargetWish, wishIds...)
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ func queryOneGroup(query string, args ...any) (*Group, error) {
|
|||
}
|
||||
|
||||
func queryManyGroupMembers(groupId string) ([]User, error) {
|
||||
query := "SELECT user.id, user.name, user.display_name, user.reference, user.is_admin, user.is_live FROM v_user AS user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ? ORDER BY group_member.user_id"
|
||||
query := "SELECT user.id, user.name, user.display_name, user.reference, user.is_admin, user.is_live, user.password_from_admin FROM v_user AS user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ? ORDER BY group_member.user_id"
|
||||
members, err := queryManyUsers(query, groupId)
|
||||
if err != nil {
|
||||
return members, err
|
||||
|
|
@ -71,8 +71,9 @@ func queryManyGroupMembers(groupId string) ([]User, error) {
|
|||
}
|
||||
|
||||
func (s *Session) GetGroupByReference(reference string) (*Group, error) {
|
||||
// FIXME: This function doesn't make much sense when there's already a public function to fetch any group, below
|
||||
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON [group].id == group_member.group_id WHERE [group].reference = ? AND group_member.user_id = ?;"
|
||||
return queryOneGroup(stmt, reference, s.User.Id)
|
||||
return queryOneGroup(stmt, reference, s.User().Id)
|
||||
}
|
||||
|
||||
func GetGroupByReference(reference string) (*Group, error) {
|
||||
|
|
@ -102,15 +103,21 @@ func (a *Admin) CreateGroup(name string, reference string) (*Group, error) {
|
|||
Name: name,
|
||||
Reference: reference,
|
||||
}
|
||||
recordEventCreateGroup(a.session.user.Id, group.Id)
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func (a *Admin) AddUserToGroup(userId, groupId string) error {
|
||||
stmt := "INSERT INTO group_member (user_id, group_id) VALUES (?, ?)"
|
||||
_, err := db.Connection.Exec(stmt, userId, groupId)
|
||||
result, err := db.Connection.Exec(stmt, userId, groupId)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("query execution failed: %w", err)
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get last insert id: %w", err)
|
||||
}
|
||||
recordEventCreateGroupMember(a.session.user.Id, strconv.FormatInt(id, 10))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -126,5 +133,5 @@ func (a *Admin) RemoveUserFromGroup(userId, groupId string) error {
|
|||
// Get the groups the session user belongs to
|
||||
func (u *Session) GetGroups() ([]Group, error) {
|
||||
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON group_member.group_id = [group].id JOIN v_user AS user ON user.id = group_member.user_id WHERE user.id = ?"
|
||||
return queryManyGroups(stmt, u.Id)
|
||||
return queryManyGroups(stmt, u.User().Id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ func TestCreateGroup(t *testing.T) {
|
|||
|
||||
fixtures.AssertEq(t, "Number of users", "My Friends", group.Name)
|
||||
fixtures.AssertEq(t, "Number of users", "my-friends", group.Reference)
|
||||
|
||||
// FIXME: disabled for now because datetimes break this
|
||||
// events, err := s.Admin().ListEvents()
|
||||
// assert.FatalErr(t, "listing events", err)
|
||||
|
||||
// assert.JsonSnapshot(t, "TestCreateGroup.snap.txt", events)
|
||||
}
|
||||
|
||||
func TestCantSeeSelfInGroup(t *testing.T) {
|
||||
|
|
@ -26,7 +32,7 @@ func TestCantSeeSelfInGroup(t *testing.T) {
|
|||
group, err := s.Admin().CreateGroup(" My Friends ", " my-friends ")
|
||||
fixtures.FailIfErr(t, err, "Failed to create group")
|
||||
|
||||
err = s.Admin().AddUserToGroup(s.User.Id, group.Id)
|
||||
err = s.Admin().AddUserToGroup(s.User().Id, group.Id)
|
||||
fixtures.FailIfErr(t, err, "Failed to add self to group")
|
||||
|
||||
err = s.Admin().AddUserToGroup(caleb.Id, group.Id)
|
||||
|
|
@ -39,4 +45,10 @@ func TestCantSeeSelfInGroup(t *testing.T) {
|
|||
fixtures.AssertEq(t, "Group contains 2 users", 2, len(group.Members))
|
||||
fixtures.AssertEq(t, "Group user 1 is thomas", "thomas", group.Members[0].Name)
|
||||
fixtures.AssertEq(t, "Group user 2 is caleb", "caleb", group.Members[1].Name)
|
||||
|
||||
// FIXME: disabled for now because datetimes break this
|
||||
// events, err := s.Admin().ListEvents()
|
||||
// assert.FatalErr(t, "listing events", err)
|
||||
|
||||
// assert.JsonSnapshot(t, "TestCantSeeSelfInGroup.snap.txt", events)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package assert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Eq[C comparable](t *testing.T, context string, expected, actual C) {
|
||||
if expected != actual {
|
||||
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func True(t *testing.T, context string, condition bool) {
|
||||
if !condition {
|
||||
t.Errorf("false: %s", context)
|
||||
}
|
||||
}
|
||||
|
||||
func FatalTrue(t *testing.T, context string, condition bool) {
|
||||
if !condition {
|
||||
t.Fatalf("%s", context)
|
||||
}
|
||||
}
|
||||
|
||||
func FatalErr(t *testing.T, context string, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
func FatalErrIs(t *testing.T, context string, err, target error) {
|
||||
if !errors.Is(err, target) {
|
||||
t.Fatalf("%s: encountered unexpected error: %s", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
func FatalErrAs(t *testing.T, context string, err error, target any) {
|
||||
if !errors.As(err, target) {
|
||||
t.Fatalf("%s: encountered unexpected error: %s", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
func SlicesEq[S ~[]E, E comparable](t *testing.T, context string, expected, actual S) {
|
||||
if !slices.Equal(expected, actual) {
|
||||
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package assert
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func pathExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !errors.Is(err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
func writeSnapshot(t *testing.T, path, actual string) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create snapshot file: %s", err)
|
||||
}
|
||||
_, err = f.Write([]byte(actual))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to snapshot file: %s", err)
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to close snapshot file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TextSnapshot(t *testing.T, path, actual string) {
|
||||
if !pathExists(path) {
|
||||
writeSnapshot(t, path, actual)
|
||||
t.Errorf("Snapshot file created: %s", path)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read snapshot file: %s", err)
|
||||
}
|
||||
expected := string(content)
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("Value doesn't match snapshot %s:\n%s", path, actual)
|
||||
}
|
||||
|
||||
if os.Getenv("UPDATE_SNAPSHOTS") == "" {
|
||||
return
|
||||
}
|
||||
|
||||
writeSnapshot(t, path, actual)
|
||||
fmt.Printf("Snapshot file %s updated\n", path)
|
||||
}
|
||||
|
||||
func JsonSnapshot(t *testing.T, path string, actual any) {
|
||||
data, err := json.MarshalIndent(actual, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Snapshot failed to serialize actual to JSON: %s", err)
|
||||
}
|
||||
TextSnapshot(t, path, string(data))
|
||||
}
|
||||
|
|
@ -15,12 +15,12 @@ var Connection *sql.DB
|
|||
func Init(dataSourceName string) error {
|
||||
db, err := sql.Open("sqlite3", dataSourceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open db connection: %w", err)
|
||||
return fmt.Errorf("failed to open db connection: %w", err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(initQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to initialize db: %w", err)
|
||||
return fmt.Errorf("failed to initialize db: %w", err)
|
||||
}
|
||||
|
||||
Connection = db
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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" (
|
||||
|
|
@ -29,15 +30,29 @@ CREATE TABLE IF NOT EXISTS "group" (
|
|||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "group_member" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"group_id" INTEGER NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT),
|
||||
UNIQUE("user_id","group_id"),
|
||||
FOREIGN KEY("group_id") REFERENCES "group"("id"),
|
||||
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "session" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"value" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL UNIQUE,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"expiry" TEXT NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT),
|
||||
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "event" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"actor_id" INTEGER NOT NULL,
|
||||
"action_type" TEXT NOT NULL,
|
||||
"target_type" TEXT NOT NULL,
|
||||
"target_id" INTEGER NOT NULL,
|
||||
"created_at" TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ')),
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE user ADD COLUMN "password_from_admin" INTEGER NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE group_member RENAME TO old_group_member;
|
||||
|
||||
CREATE TABLE "group_member" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"group_id" INTEGER NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT),
|
||||
UNIQUE("user_id","group_id"),
|
||||
FOREIGN KEY("group_id") REFERENCES "group"("id"),
|
||||
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
||||
);
|
||||
|
||||
INSERT INTO group_member (group_id, user_id) SELECT group_id, user_id FROM old_group_member;
|
||||
|
||||
DROP TABLE "old_group_member";
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -2,24 +2,28 @@ package fixtures
|
|||
|
||||
import "testing"
|
||||
|
||||
// Deprecated: use internal/assert
|
||||
func AssertEq[C comparable](t *testing.T, context string, expected, actual C) {
|
||||
if expected != actual {
|
||||
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: use internal/assert
|
||||
func Assert(t *testing.T, context string, condition bool) {
|
||||
if !condition {
|
||||
t.Errorf("%s", context)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: use internal/assert
|
||||
func FatalAssert(t *testing.T, context string, condition bool) {
|
||||
if !condition {
|
||||
t.Fatalf("%s", context)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: use internal/assert
|
||||
func FailIfErr(t *testing.T, err error, context string) {
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s\n", context, err)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package fixtures
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
|
||||
|
|
@ -14,21 +14,36 @@ func TestInit(t *testing.T) error {
|
|||
return lishwist.Init(uri)
|
||||
}
|
||||
|
||||
// Deprecated: This function also inits the test, which prevents it from being used more than once per test
|
||||
func Login(t *testing.T, username, password string) *lishwist.Session {
|
||||
uri := memdb.TestDB(t)
|
||||
err := lishwist.Init(uri)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to init db: %s\n", err)
|
||||
t.Fatalf("Failed to init db: %s\n", err)
|
||||
}
|
||||
|
||||
_, err = lishwist.Register(username, password)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to register on login fixture: %s\n", err)
|
||||
t.Fatalf("Failed to register on login fixture: %s\n", err)
|
||||
}
|
||||
|
||||
session, err := lishwist.Login(username, password)
|
||||
session, err := lishwist.Login(username, password, time.Hour*24)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to login on fixture: %s\n", err)
|
||||
t.Fatalf("Failed to login on fixture: %s\n", err)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func Login2(t *testing.T, username, password string) *lishwist.Session {
|
||||
_, err := lishwist.Register(username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register on login fixture: %s\n", err)
|
||||
}
|
||||
|
||||
session, err := lishwist.Login(username, password, time.Hour*24)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login on fixture: %s\n", err)
|
||||
}
|
||||
|
||||
return session
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package id
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func Generate() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
|
@ -2,13 +2,14 @@ package lishwist
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type ErrorInvalidCredentials error
|
||||
|
||||
func Login(username, password string) (*Session, error) {
|
||||
func Login(username, password string, sessionMaxAge time.Duration) (*Session, error) {
|
||||
user, err := getUserByName(username)
|
||||
if err != nil {
|
||||
return nil, ErrorInvalidCredentials(fmt.Errorf("Failed to fetch user: %w", err))
|
||||
|
|
@ -27,5 +28,10 @@ func Login(username, password string) (*Session, error) {
|
|||
return nil, ErrorInvalidCredentials(fmt.Errorf("Password compare failed: %w", err))
|
||||
}
|
||||
|
||||
return &Session{*user}, nil
|
||||
session, err := insertSession(*user, sessionMaxAge)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert session: %w", err)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package lishwist_test
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/core/internal/fixtures"
|
||||
|
|
@ -18,7 +19,7 @@ func TestLogin(t *testing.T) {
|
|||
t.Fatalf("Failed to register: %s\n", err)
|
||||
}
|
||||
|
||||
_, err = lishwist.Login("thomas", "123")
|
||||
_, err = lishwist.Login("thomas", "123", time.Hour*24)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %s\n", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\n", err)
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
|
|
|||
|
|
@ -1,15 +1,67 @@
|
|||
package lishwist
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lishwist/core/internal/db"
|
||||
"lishwist/core/internal/id"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
User
|
||||
user User
|
||||
Key string
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
func SessionFromUsername(username string) (*Session, error) {
|
||||
user, err := getUserByName(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get user: %w", err)
|
||||
}
|
||||
return &Session{*user}, nil
|
||||
// Returns a copy of the user associated with this session
|
||||
func (s *Session) User() User {
|
||||
return s.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 = ?"
|
||||
var expiry string
|
||||
err := db.Connection.QueryRow(query, key).Scan(
|
||||
&s.user.Id,
|
||||
&s.user.NormalName,
|
||||
&s.user.Name,
|
||||
&s.user.Reference,
|
||||
&s.user.IsAdmin,
|
||||
&s.user.IsLive,
|
||||
&s.user.PasswordFromAdmin,
|
||||
&s.Key,
|
||||
&expiry,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch session: %w", err)
|
||||
}
|
||||
s.Expiry, err = time.Parse(time.RFC3339Nano, expiry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse session expiry: %w", err)
|
||||
}
|
||||
if time.Now().After(s.Expiry) {
|
||||
return nil, nil
|
||||
}
|
||||
return &s, err
|
||||
}
|
||||
|
||||
func insertSession(user User, maxAge time.Duration) (*Session, error) {
|
||||
s := Session{
|
||||
user: user,
|
||||
Key: id.Generate(),
|
||||
Expiry: time.Now().Add(maxAge),
|
||||
}
|
||||
stmt := "INSERT INTO session (key, user_id, expiry) VALUES (?, ?, ?)"
|
||||
_, err := db.Connection.Exec(stmt, &s.Key, &user.Id, &s.Expiry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"lishwist/core/internal/db"
|
||||
|
||||
"github.com/Teajey/sqlstore"
|
||||
)
|
||||
|
||||
func NewStore(keyPairs ...[]byte) (*sqlstore.Store, error) {
|
||||
deleteStmt, err := db.Connection.Prepare("DELETE FROM session WHERE id = ?;")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to prepare delete statement: %w", err)
|
||||
}
|
||||
|
||||
insertStmt, err := db.Connection.Prepare("INSERT INTO session (value) VALUES (?);")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to prepare insert statement: %w", err)
|
||||
}
|
||||
|
||||
selectStmt, err := db.Connection.Prepare("SELECT value FROM session WHERE id = ?;")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to prepare select statement: %w", err)
|
||||
}
|
||||
|
||||
updateStmt, err := db.Connection.Prepare("UPDATE session SET value = ?2 WHERE id = ?1;")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to prepare update statement: %w", err)
|
||||
}
|
||||
|
||||
s := sqlstore.NewSqlStore(db.Connection, sqlstore.Statements{
|
||||
Delete: deleteStmt,
|
||||
Insert: insertStmt,
|
||||
Select: selectStmt,
|
||||
Update: updateStmt,
|
||||
}, keyPairs...)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
65
core/user.go
65
core/user.go
|
|
@ -4,19 +4,20 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"lishwist/core/internal/db"
|
||||
"lishwist/core/internal/normalize"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id string
|
||||
// TODO: rename to DisplayName
|
||||
NormalName string
|
||||
Name string
|
||||
Reference string
|
||||
IsAdmin bool
|
||||
IsLive bool
|
||||
Id string
|
||||
NormalName string
|
||||
Name string
|
||||
Reference string
|
||||
IsAdmin bool
|
||||
IsLive bool
|
||||
PasswordFromAdmin bool
|
||||
}
|
||||
|
||||
func queryManyUsers(query string, args ...any) ([]User, error) {
|
||||
|
|
@ -28,7 +29,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)
|
||||
err = rows.Scan(&u.Id, &u.NormalName, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive, &u.PasswordFromAdmin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -54,7 +55,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 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)
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +78,7 @@ func createUser(name string, passHash []byte, isAdmin bool) (*User, error) {
|
|||
Id: fmt.Sprintf("%d", id),
|
||||
Name: name,
|
||||
}
|
||||
recordEventCreateUser(user.Id, user.Id)
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
|
|
@ -91,12 +93,12 @@ func (u *User) getPassHash() ([]byte, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +113,7 @@ func hasUsers() (bool, 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)
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +126,7 @@ func GetUserByReference(reference string) (*User, error) {
|
|||
}
|
||||
|
||||
func (u *User) GetTodo() ([]Wish, error) {
|
||||
stmt := "SELECT wish.id, wish.name, wish.sent, recipient.name, recipient.reference FROM wish JOIN v_user AS user ON wish.claimant_id = user.id JOIN v_user AS recipient ON wish.recipient_id = recipient.id WHERE user.id = ? ORDER BY wish.sent ASC, wish.name"
|
||||
stmt := "SELECT wish.id, wish.name, wish.sent, recipient.display_name, recipient.reference FROM wish JOIN v_user AS user ON wish.claimant_id = user.id JOIN v_user AS recipient ON wish.recipient_id = recipient.id WHERE user.id = ? ORDER BY wish.sent ASC, wish.name"
|
||||
rows, err := db.Connection.Query(stmt, u.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -160,6 +162,41 @@ func (u *Admin) UserSetLive(userReference string, setting bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// u.IsLive = setting
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *Admin) RenameUser(userReference string, displayName string) error {
|
||||
name := normalize.Name(displayName)
|
||||
query := "UPDATE user SET name = ?, display_name = ? WHERE reference = ?"
|
||||
_, err := db.Connection.Exec(query, name, displayName, userReference)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
64
core/wish.go
64
core/wish.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"lishwist/core/internal/db"
|
||||
|
|
@ -23,8 +24,8 @@ type Wish struct {
|
|||
}
|
||||
|
||||
func (s *Session) GetWishes() ([]Wish, error) {
|
||||
stmt := "SELECT wish.id, wish.name, wish.sent FROM wish WHERE wish.creator_id = ?1 AND wish.recipient_id = ?1"
|
||||
rows, err := db.Connection.Query(stmt, s.User.Id)
|
||||
stmt := "SELECT wish.id, wish.name, wish.sent FROM wish WHERE wish.creator_id = ?1 AND wish.recipient_id = ?1 ORDER BY wish.sent"
|
||||
rows, err := db.Connection.Query(stmt, s.User().Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Query execution failed: %w", err)
|
||||
}
|
||||
|
|
@ -52,19 +53,26 @@ func (s *Session) GetWishes() ([]Wish, error) {
|
|||
return wishs, nil
|
||||
}
|
||||
|
||||
func (s *Session) MakeWish(name string) error {
|
||||
// May return the id of the wish
|
||||
func (s *Session) MakeWish(name string) (string, error) {
|
||||
stmt := "INSERT INTO wish (name, recipient_id, creator_id) VALUES (?, ?, ?)"
|
||||
_, err := db.Connection.Exec(stmt, strings.TrimSpace(name), s.User.Id, s.User.Id)
|
||||
result, err := db.Connection.Exec(stmt, strings.TrimSpace(name), s.User().Id, s.User().Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query execution failed: %w", err)
|
||||
return "", fmt.Errorf("Query execution failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get last insert id: %w", err)
|
||||
}
|
||||
wishId := strconv.FormatInt(id, 10)
|
||||
recordEventCreateWish(s.user.Id, wishId)
|
||||
return wishId, nil
|
||||
}
|
||||
|
||||
func (u *Session) deleteWishes(tx *sql.Tx, ids []string) error {
|
||||
func (s *Session) deleteWishes(tx *sql.Tx, ids []string) error {
|
||||
stmt := "DELETE FROM wish WHERE wish.creator_id = ? AND wish.id = ?"
|
||||
for _, id := range ids {
|
||||
r, err := tx.Exec(stmt, u.Id, id)
|
||||
r, err := tx.Exec(stmt, s.User().Id, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -107,10 +115,10 @@ func (s *Session) GetOthersWishes(userReference string) ([]Wish, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get other user: %w", err)
|
||||
}
|
||||
if otherUser.Id == s.User.Id {
|
||||
if otherUser.Id == s.User().Id {
|
||||
return nil, errors.New("Use (s *Session) GetWishes() to view your own wishes")
|
||||
}
|
||||
stmt := "SELECT wish.id, wish.name, claimant.id, claimant.name, wish.sent, wish.creator_id, creator.name, wish.recipient_id FROM wish JOIN v_user AS user ON wish.recipient_id = user.id LEFT JOIN v_user AS claimant ON wish.claimant_id = claimant.id LEFT JOIN v_user AS creator ON wish.creator_id = creator.id WHERE user.id = ?"
|
||||
stmt := "SELECT wish.id, wish.name, claimant.id, claimant.display_name, wish.sent, wish.creator_id, creator.display_name, wish.recipient_id FROM wish JOIN v_user AS user ON wish.recipient_id = user.id LEFT JOIN v_user AS claimant ON wish.claimant_id = claimant.id LEFT JOIN v_user AS creator ON wish.creator_id = creator.id WHERE user.id = ? ORDER BY wish.sent"
|
||||
rows, err := db.Connection.Query(stmt, otherUser.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to execute query: %w", err)
|
||||
|
|
@ -164,7 +172,7 @@ func (s *Session) executeClaims(tx *sql.Tx, claims, unclaims []string) error {
|
|||
claimStmt := "UPDATE wish SET claimant_id = ? WHERE id = ?"
|
||||
unclaimStmt := "UPDATE wish SET claimant_id = NULL WHERE id = ?"
|
||||
for _, id := range claims {
|
||||
r, err := tx.Exec(claimStmt, s.Id, id)
|
||||
r, err := tx.Exec(claimStmt, s.User().Id, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -194,7 +202,10 @@ func (s *Session) executeClaims(tx *sql.Tx, claims, unclaims []string) error {
|
|||
|
||||
// Undertake or abandon wishes made by other users
|
||||
func (s *Session) ClaimWishes(claims, unclaims []string) error {
|
||||
if len(claims) < 1 && len(unclaims) < 1 {
|
||||
lenClaims := len(claims)
|
||||
lenUnclaims := len(unclaims)
|
||||
// TODO: Would be nice if this used a request builder
|
||||
if lenClaims < 1 && lenUnclaims < 1 {
|
||||
return fmt.Errorf("Attempt to claim/unclaim zero wishes")
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +224,17 @@ func (s *Session) ClaimWishes(claims, unclaims []string) error {
|
|||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: This could be atomic. See recordEvent function
|
||||
if lenClaims > 0 {
|
||||
recordEventClaimWishes(s.user.Id, claims...)
|
||||
}
|
||||
if lenUnclaims > 0 {
|
||||
recordEventUnclaimWishes(s.user.Id, unclaims...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeCompletions(tx *sql.Tx, claims []string) error {
|
||||
|
|
@ -234,8 +255,8 @@ func executeCompletions(tx *sql.Tx, claims []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: User ought not be able to interact with wishes outside their group network
|
||||
func (s *Session) CompleteWishes(claims []string) error {
|
||||
// TODO: User ought not be able to interact with wishes outside their group network
|
||||
if len(claims) < 1 {
|
||||
return fmt.Errorf("Attempt to complete zero wishes")
|
||||
}
|
||||
|
|
@ -255,7 +276,13 @@ func (s *Session) CompleteWishes(claims []string) error {
|
|||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordEventCompleteWishes(s.user.Id, claims...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Session) SuggestWishForUser(otherUserReference string, wishName string) error {
|
||||
|
|
@ -264,7 +291,7 @@ func (u *Session) SuggestWishForUser(otherUserReference string, wishName string)
|
|||
return err
|
||||
}
|
||||
stmt := "INSERT INTO wish (name, recipient_id, creator_id) VALUES (?, ?, ?)"
|
||||
_, err = db.Connection.Exec(stmt, wishName, otherUser.Id, u.Id)
|
||||
_, err = db.Connection.Exec(stmt, wishName, otherUser.Id, u.User().Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -291,5 +318,8 @@ func (s *Session) RecindWishesForUser(ids ...string) error {
|
|||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ package lishwist_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"lishwist/core/internal/assert"
|
||||
"lishwist/core/internal/fixtures"
|
||||
)
|
||||
|
||||
func TestMakeWish(t *testing.T) {
|
||||
s := fixtures.Login(t, "thomas", "123")
|
||||
|
||||
if err := s.MakeWish("apple"); err != nil {
|
||||
if _, err := s.MakeWish("apple"); err != nil {
|
||||
t.Fatalf("Failed to make wish 1: %s\n", err)
|
||||
}
|
||||
|
||||
if err := s.MakeWish(" A car "); err != nil {
|
||||
if _, err := s.MakeWish(" A car "); err != nil {
|
||||
t.Fatalf("Failed to make wish 2: %s\n", err)
|
||||
}
|
||||
|
||||
|
|
@ -26,3 +27,35 @@ func TestMakeWish(t *testing.T) {
|
|||
fixtures.AssertEq(t, "Wish 1 name", wishes[0].Name, "apple")
|
||||
fixtures.AssertEq(t, "Wish 2 name", wishes[1].Name, "A car")
|
||||
}
|
||||
|
||||
func TestClaimUnclaimWishes(t *testing.T) {
|
||||
err := fixtures.TestInit(t)
|
||||
assert.FatalErr(t, "initializing db", err)
|
||||
|
||||
thomas := fixtures.Login2(t, "thomas", "123")
|
||||
assert.FatalErr(t, "registering thomas", err)
|
||||
|
||||
caleb := fixtures.Login2(t, "caleb", "123")
|
||||
assert.FatalErr(t, "registering caleb", err)
|
||||
|
||||
food, err := caleb.MakeWish("food")
|
||||
assert.FatalErr(t, "making wish 1", err)
|
||||
|
||||
box, err := caleb.MakeWish("box")
|
||||
assert.FatalErr(t, "making wish 2", err)
|
||||
|
||||
drink, err := caleb.MakeWish("drink")
|
||||
assert.FatalErr(t, "making wish 3", err)
|
||||
|
||||
err = thomas.ClaimWishes([]string{food, box, drink}, []string{})
|
||||
assert.FatalErr(t, "claiming wishes", err)
|
||||
|
||||
err = thomas.ClaimWishes([]string{}, []string{food, box, drink})
|
||||
assert.FatalErr(t, "unclaiming wishes", err)
|
||||
|
||||
// FIXME: disabled for now because datetimes break this
|
||||
// events, err := thomas.Admin().ListEvents()
|
||||
// assert.FatalErr(t, "listing events", err)
|
||||
|
||||
// assert.JsonSnapshot(t, "TestClaimUnclaimWishes.snap.txt", events)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
type LoginProps struct {
|
||||
GeneralError string `json:",omitempty"`
|
||||
SuccessfulRegistration bool `json:",omitempty"`
|
||||
SuccessfulSetPassword bool `json:",omitempty"`
|
||||
Username templates.InputProps
|
||||
Password templates.InputProps
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
)
|
||||
|
||||
type RegisterProps struct {
|
||||
Navbar templates.NavCollapse
|
||||
GeneralError string `json:",omitempty"`
|
||||
Username templates.InputProps
|
||||
Password templates.InputProps
|
||||
|
|
@ -36,6 +37,7 @@ func (p *RegisterProps) Validate() (valid bool) {
|
|||
|
||||
func NewRegisterProps(usernameVal, passwordVal, confirmPassVal string) *RegisterProps {
|
||||
return &RegisterProps{
|
||||
Navbar: templates.DefaultNavCollapse(),
|
||||
GeneralError: "",
|
||||
Username: templates.InputProps{
|
||||
Name: "username",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
top_level=$(git rev-parse --show-toplevel)
|
||||
git_version=$($top_level/scripts/git-version)
|
||||
|
||||
go build -ldflags=-X=lishwist/http/env.GitVersion=$git_version .
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
top_level=$(git rev-parse --show-toplevel)
|
||||
git_version=$($top_level/scripts/git-version)
|
||||
|
||||
go generate ../core/internal/db
|
||||
go run -ldflags=-X=lishwist/http/env.GitVersion=$git_version main.go
|
||||
|
|
@ -6,28 +6,42 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
func GuaranteeEnv(key string) (variable string) {
|
||||
func GuaranteeEnv(key string) string {
|
||||
variable, ok := os.LookupEnv(key)
|
||||
if !ok || variable == "" {
|
||||
log.Fatalln("Missing environment variable:", key)
|
||||
}
|
||||
return
|
||||
return variable
|
||||
}
|
||||
|
||||
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
|
||||
}()
|
||||
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()
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package env
|
||||
|
||||
var GitVersion string
|
||||
23
http/go.mod
23
http/go.mod
|
|
@ -1,25 +1,18 @@
|
|||
module lishwist/http
|
||||
|
||||
go 1.23
|
||||
go 1.23.3
|
||||
|
||||
toolchain go1.23.3
|
||||
toolchain go1.24.5
|
||||
|
||||
require (
|
||||
github.com/Teajey/sqlstore v0.0.6
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/Teajey/rsvp v0.13.1
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
)
|
||||
|
||||
require github.com/gorilla/securecookie v1.1.2
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
modernc.org/libc v1.37.6 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
)
|
||||
|
|
|
|||
49
http/go.sum
49
http/go.sum
|
|
@ -1,39 +1,22 @@
|
|||
github.com/Teajey/sqlstore v0.0.3 h1:6Y1jz9/yw1cj/Z/jrii0s87RAomKWr/07B1auDgw8pg=
|
||||
github.com/Teajey/sqlstore v0.0.3/go.mod h1:hjk0S593/2Q4QxkEXCgpThj9w5KWGTQi9JtgfziHXXk=
|
||||
github.com/Teajey/sqlstore v0.0.4 h1:ATe25BD8cV0FUw4w2qlccx5m0c5kQI0K4ksl/LnSHsc=
|
||||
github.com/Teajey/sqlstore v0.0.4/go.mod h1:hjk0S593/2Q4QxkEXCgpThj9w5KWGTQi9JtgfziHXXk=
|
||||
github.com/Teajey/sqlstore v0.0.5 h1:WZvu54baa8+9n1sKQe9GuxBVwSISw+xCkw4VFSwwIs8=
|
||||
github.com/Teajey/sqlstore v0.0.5/go.mod h1:hjk0S593/2Q4QxkEXCgpThj9w5KWGTQi9JtgfziHXXk=
|
||||
github.com/Teajey/sqlstore v0.0.6 h1:kUEpA+3BKFHZl128MuMeYY6zVcmq1QmOlNyofcFEJOA=
|
||||
github.com/Teajey/sqlstore v0.0.6/go.mod h1:hjk0S593/2Q4QxkEXCgpThj9w5KWGTQi9JtgfziHXXk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/Teajey/rsvp v0.13.1 h1:0lw+JosaWmdjSmXoKQYBRS9nptSZPInm60Y5GQ3llEU=
|
||||
github.com/Teajey/rsvp v0.13.1/go.mod h1:z0L20VphVg+Ec2+hnpLFTG2MZTrWYFprav1kpxDba0Q=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
|
||||
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package id
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func Generate() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
54
http/main.go
54
http/main.go
|
|
@ -1,67 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/core/session"
|
||||
"lishwist/http/api"
|
||||
"lishwist/http/env"
|
||||
"lishwist/http/router"
|
||||
"lishwist/http/routing"
|
||||
"lishwist/http/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gob.Register(&api.RegisterProps{})
|
||||
gob.Register(&api.LoginProps{})
|
||||
|
||||
err := lishwist.Init(env.DatabaseFile)
|
||||
err := lishwist.Init(env.Configuration.DatabaseFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to init Lishwist: %s\n", err)
|
||||
}
|
||||
|
||||
store, err := session.NewStore([]byte(env.SessionSecret))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize session store: %s\n", err)
|
||||
}
|
||||
store.Options.MaxAge = 86_400
|
||||
store.Options.Secure = !env.InDev
|
||||
store.Options.HttpOnly = true
|
||||
useSecureCookies := !env.Configuration.InDev
|
||||
r := server.Create(useSecureCookies)
|
||||
|
||||
r := router.New(store)
|
||||
|
||||
r.Public.HandleFunc("GET /", routing.Login)
|
||||
r.Public.HandleFunc("GET /groups/{groupReference}", routing.PublicGroup)
|
||||
r.Public.HandleFunc("GET /lists/{userReference}", routing.PublicWishlist)
|
||||
r.Public.HandleFunc("GET /register", routing.Register)
|
||||
r.Public.HandleFunc("POST /", routing.LoginPost)
|
||||
r.Public.HandleFunc("POST /register", routing.RegisterPost)
|
||||
|
||||
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))
|
||||
r.Private.HandleFunc("GET /lists/{userReference}", routing.ExpectAppSession(routing.ForeignWishlist))
|
||||
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 /groups/{groupReference}", routing.ExpectAppSession(routing.GroupPost))
|
||||
r.Private.HandleFunc("POST /list/{userReference}", routing.ExpectAppSession(routing.ForeignWishlistPost))
|
||||
r.Private.HandleFunc("POST /logout", routing.LogoutPost)
|
||||
r.Private.HandleFunc("POST /users/{userReference}", routing.ExpectAppSession(routing.UserPost))
|
||||
r.Private.HandleFunc("POST /{$}", routing.ExpectAppSession(routing.HomePost))
|
||||
|
||||
// Deprecated
|
||||
r.Private.HandleFunc("GET /group/{groupReference}", routing.ExpectAppSession(routing.Group))
|
||||
r.Private.HandleFunc("GET /list/{userReference}", routing.ExpectAppSession(routing.ForeignWishlist))
|
||||
r.Public.HandleFunc("GET /group/{groupReference}", routing.PublicGroup)
|
||||
r.Public.HandleFunc("GET /list/{userReference}", routing.PublicWishlist)
|
||||
|
||||
http.Handle("/", r)
|
||||
|
||||
log.Printf("Running at http://127.0.0.1:%s\n", env.ServePort)
|
||||
err = http.ListenAndServe(":"+env.ServePort, nil)
|
||||
log.Printf("Running at http://127.0.0.1:%s\n", env.Configuration.ServePort)
|
||||
err = http.ListenAndServe(":"+env.Configuration.ServePort, r)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to listen and server:", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"lishwist/http/session"
|
||||
"lishwist/http/templates"
|
||||
"lishwist/http/templates/text"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
type ServeMux struct {
|
||||
inner *rsvp.ServeMux
|
||||
store *session.Store
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
m.inner.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
ServeHTTP(*Session, http.Header, *http.Request) rsvp.Response
|
||||
}
|
||||
|
||||
type HandlerFunc func(*Session, http.Header, *http.Request) rsvp.Response
|
||||
|
||||
func (m *ServeMux) HandleFunc(pattern string, handler HandlerFunc) {
|
||||
m.inner.MiddleHandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) rsvp.Response {
|
||||
session := m.GetSession(r)
|
||||
|
||||
response := handler(session, w.Header(), r)
|
||||
|
||||
if session.written {
|
||||
err := session.inner.Save(r, w)
|
||||
if err != nil {
|
||||
log.Printf("Failed to save session: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
func (m *ServeMux) Handle(pattern string, handler Handler) {
|
||||
m.HandleFunc(pattern, handler.ServeHTTP)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (m *ServeMux) GetSession(r *http.Request) *Session {
|
||||
session, _ := m.store.Get(r, "lishwist_user")
|
||||
return &Session{inner: session}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"lishwist/http/templates"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func NotFound() rsvp.Response {
|
||||
return Error(http.StatusNotFound, "Page not found")
|
||||
}
|
||||
|
||||
type errorProps struct {
|
||||
Navbar templates.NavCollapse
|
||||
Message string
|
||||
}
|
||||
|
||||
func Error(status int, format string, a ...any) rsvp.Response {
|
||||
return rsvp.Response{
|
||||
Body: errorProps{
|
||||
Message: fmt.Sprintf(format, a...),
|
||||
Navbar: templates.DefaultNavCollapse(),
|
||||
},
|
||||
Status: status,
|
||||
TemplateName: "error_page.gotmpl",
|
||||
}
|
||||
}
|
||||
|
||||
func Data(templateName string, body any) rsvp.Response {
|
||||
return rsvp.Response{
|
||||
Body: body,
|
||||
TemplateName: templateName,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +1,52 @@
|
|||
package rsvp
|
||||
package response
|
||||
|
||||
import (
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
inner *sessions.Session
|
||||
inner *sessions.Session
|
||||
written bool
|
||||
}
|
||||
|
||||
const flashKey = "_flash"
|
||||
|
||||
func (s *Session) FlashGet() any {
|
||||
list := s.inner.Flashes()
|
||||
if len(list) < 1 {
|
||||
return nil
|
||||
} else {
|
||||
return list[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) FlashPeek() any {
|
||||
flash, ok := s.inner.Values["_flash"]
|
||||
val, ok := s.inner.Values[flashKey]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
list := flash.([]any)
|
||||
if len(list) < 1 {
|
||||
delete(s.inner.Values, flashKey)
|
||||
s.written = true
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *Session) FlashPeek() any {
|
||||
val, ok := s.inner.Values[flashKey]
|
||||
if !ok {
|
||||
return nil
|
||||
} else {
|
||||
return list[0]
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *Session) FlashSet(value any) {
|
||||
s.inner.AddFlash(value)
|
||||
s.inner.Values[flashKey] = value
|
||||
s.written = true
|
||||
}
|
||||
|
||||
func (s *Session) SetID(value string) {
|
||||
s.inner.ID = value
|
||||
s.written = true
|
||||
}
|
||||
|
||||
func (s *Session) SetValue(key any, value any) {
|
||||
s.inner.Values[key] = value
|
||||
s.written = true
|
||||
}
|
||||
|
||||
func (s *Session) RemoveValue(key any) {
|
||||
delete(s.inner.Values, key)
|
||||
s.written = true
|
||||
}
|
||||
|
||||
func (s *Session) GetValue(key any) any {
|
||||
|
|
@ -47,7 +54,8 @@ func (s *Session) GetValue(key any) any {
|
|||
}
|
||||
|
||||
func (s *Session) ClearValues() {
|
||||
s.inner.Values = nil
|
||||
s.inner.Values = make(map[any]any)
|
||||
s.written = true
|
||||
}
|
||||
|
||||
func (s *Session) Options() *sessions.Options {
|
||||
|
|
@ -1,33 +1,38 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"lishwist/http/rsvp"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/sqlstore"
|
||||
"lishwist/http/response"
|
||||
"lishwist/http/session"
|
||||
)
|
||||
|
||||
type VisibilityRouter struct {
|
||||
Store *sqlstore.Store
|
||||
Public *rsvp.ServeMux
|
||||
Private *rsvp.ServeMux
|
||||
store *session.Store
|
||||
Public *response.ServeMux
|
||||
Private *response.ServeMux
|
||||
}
|
||||
|
||||
func (s *VisibilityRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := s.Store.Get(r, "lishwist_user")
|
||||
authorized, _ := session.Values["authorized"].(bool)
|
||||
session, _ := s.store.Get(r, "lishwist_user")
|
||||
_, inSession := session.Values["sessionKey"]
|
||||
|
||||
if authorized {
|
||||
if inSession {
|
||||
s.Private.ServeHTTP(w, r)
|
||||
} else {
|
||||
s.Public.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func New(store *sqlstore.Store) *VisibilityRouter {
|
||||
func New(store *session.Store) *VisibilityRouter {
|
||||
return &VisibilityRouter{
|
||||
Store: store,
|
||||
Public: rsvp.NewServeMux(store),
|
||||
Private: rsvp.NewServeMux(store),
|
||||
store: store,
|
||||
Public: response.NewServeMux(store),
|
||||
Private: response.NewServeMux(store),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *VisibilityRouter) HandleFunc(pattern string, handler response.HandlerFunc) {
|
||||
r.Public.HandleFunc(pattern, handler)
|
||||
r.Private.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
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 {
|
||||
Navbar templates.NavCollapse
|
||||
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{
|
||||
Navbar: templates.NavCollapse{
|
||||
Links: []templates.Link{{Href: "/", Name: "Home"}},
|
||||
User: &templates.User{Name: 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,
|
||||
}}
|
||||
}
|
||||
|
|
@ -1,24 +1,34 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/rsvp"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
|
||||
"lishwist/http/response"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func ExpectAppSession(next func(*lishwist.Session, http.Header, *rsvp.Request) rsvp.Response) rsvp.HandlerFunc {
|
||||
return func(w http.Header, r *rsvp.Request) rsvp.Response {
|
||||
session := r.GetSession()
|
||||
username, ok := session.GetValue("username").(string)
|
||||
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 {
|
||||
sessionKey, ok := session.GetValue("sessionKey").(string)
|
||||
if !ok {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get username from session")
|
||||
log.Printf("Failed to get key from session\n")
|
||||
return response.Error(http.StatusInternalServerError, "Something went wrong.")
|
||||
}
|
||||
|
||||
appSession, err := lishwist.SessionFromUsername(username)
|
||||
appSession, err := lishwist.SessionFromKey(sessionKey)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get session by username %q: %s", username, err)
|
||||
log.Printf("Failed to get session by key %v: %s\n", sessionKey, err)
|
||||
return response.Error(http.StatusInternalServerError, "Something went wrong.")
|
||||
}
|
||||
if appSession == nil {
|
||||
log.Printf("Session not found under key: %s\n", sessionKey)
|
||||
return response.Error(http.StatusInternalServerError, "Something went wrong.")
|
||||
}
|
||||
|
||||
return next(appSession, w, r)
|
||||
return next(appSession, session, h, r)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeGeneralErrorJson(w http.ResponseWriter, status int, format string, a ...any) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
log.Printf("General error: %s\n", msg)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
escapedMsg := strings.ReplaceAll(msg, `"`, `\"`)
|
||||
_, _ = w.Write([]byte(fmt.Sprintf(`{"GeneralError":"%s"}`, escapedMsg)))
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/response"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
type Events struct {
|
||||
Events []lishwist.Event `xml:"Event"`
|
||||
}
|
||||
|
||||
func EventList(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
admin := app.Admin()
|
||||
if admin == nil {
|
||||
log.Println("Attempt to access EventList by non-admin. Responding 404 Not Found.")
|
||||
return response.NotFound()
|
||||
}
|
||||
|
||||
events, err := admin.ListEvents()
|
||||
if err != nil {
|
||||
log.Printf("Admin failed to ListEvents: %s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get events: %s", err)
|
||||
}
|
||||
|
||||
return response.Data("", Events{Events: events})
|
||||
}
|
||||
|
|
@ -2,55 +2,66 @@ package routing
|
|||
|
||||
import (
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
"lishwist/http/templates"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
type foreignWishlistProps struct {
|
||||
Navbar templates.NavCollapse
|
||||
CurrentUserId string
|
||||
CurrentUserName string
|
||||
Username string
|
||||
Gifts []lishwist.Wish
|
||||
}
|
||||
|
||||
func ForeignWishlist(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func ForeignWishlist(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
userReference := r.PathValue("userReference")
|
||||
if app.User.Reference == userReference {
|
||||
return rsvp.SeeOther("/")
|
||||
user := app.User()
|
||||
if user.Reference == userReference {
|
||||
return rsvp.Found("/", "You're not allowed to view your own wishlist!")
|
||||
}
|
||||
otherUser, err := lishwist.GetUserByReference(userReference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q: %s", userReference, err)
|
||||
log.Printf("Couldn't get user by reference %q: %s\n", userReference, err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(")
|
||||
}
|
||||
if otherUser == nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "User not found")
|
||||
return response.Error(http.StatusInternalServerError, "User not found")
|
||||
}
|
||||
wishes, err := app.GetOthersWishes(userReference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("%q couldn't get wishes of other user %q: %s", app.User.Name, otherUser.Name, err)
|
||||
log.Printf("%q couldn't get wishes of other user %q: %s\n", user.Name, otherUser.Name, err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(")
|
||||
}
|
||||
p := foreignWishlistProps{CurrentUserId: app.User.Id, CurrentUserName: app.User.Name, Username: otherUser.Name, Gifts: wishes}
|
||||
return rsvp.Data("foreign_wishlist.gotmpl", p)
|
||||
p := foreignWishlistProps{Navbar: templates.UserNavCollapse(user.Name, user.PasswordFromAdmin), CurrentUserId: user.Id, Username: otherUser.Name, Gifts: wishes}
|
||||
return response.Data("foreign_wishlist.gotmpl", p)
|
||||
}
|
||||
|
||||
type publicWishlistProps struct {
|
||||
Navbar templates.NavCollapse
|
||||
Username string
|
||||
GiftCount int
|
||||
}
|
||||
|
||||
func PublicWishlist(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func PublicWishlist(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
userReference := r.PathValue("userReference")
|
||||
otherUser, err := lishwist.GetUserByReference(userReference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q on public wishlist: %s", userReference, err)
|
||||
log.Printf("Couldn't get user by reference %q on public wishlist: %s\n", userReference, err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(")
|
||||
}
|
||||
if otherUser == nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "User not found")
|
||||
return response.Error(http.StatusInternalServerError, "User not found")
|
||||
}
|
||||
giftCount, err := otherUser.WishCount()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get wishes of user %q on public wishlist: %s", otherUser.Name, err)
|
||||
log.Printf("Couldn't get wishes of user %q on public wishlist: %s\n", otherUser.Name, err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(")
|
||||
}
|
||||
p := publicWishlistProps{Username: otherUser.Name, GiftCount: giftCount}
|
||||
return rsvp.Data("public_foreign_wishlist.gotmpl", p)
|
||||
p := publicWishlistProps{Navbar: templates.DefaultNavCollapse(), Username: otherUser.Name, GiftCount: giftCount}
|
||||
return response.Data("public_foreign_wishlist.gotmpl", p)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,126 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
"lishwist/http/templates"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
type GroupProps struct {
|
||||
Group *lishwist.Group
|
||||
CurrentUsername string
|
||||
Navbar templates.NavCollapse
|
||||
Group *lishwist.Group
|
||||
}
|
||||
|
||||
func AdminGroup(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func AdminGroup(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
reference := r.PathValue("groupReference")
|
||||
group, err := app.GetGroupByReference(reference)
|
||||
group, err := lishwist.GetGroupByReference(reference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Couldn't get group: %s", err)
|
||||
return response.Error(http.StatusInternalServerError, "Couldn't get group: %s", err)
|
||||
}
|
||||
if group == nil {
|
||||
return rsvp.Error(http.StatusNotFound, "Group not found")
|
||||
return response.Error(http.StatusNotFound, "Group not found")
|
||||
}
|
||||
if !app.User.IsAdmin {
|
||||
index := group.MemberIndex(app.User.Id)
|
||||
user := app.User()
|
||||
if !user.IsAdmin {
|
||||
index := group.MemberIndex(user.Id)
|
||||
group.Members = slices.Delete(group.Members, index, index+1)
|
||||
}
|
||||
p := GroupProps{
|
||||
Group: group,
|
||||
CurrentUsername: app.User.Name,
|
||||
Navbar: templates.UserNavCollapse(user.Name, user.PasswordFromAdmin),
|
||||
Group: group,
|
||||
}
|
||||
return rsvp.Data("group_page.gotmpl", p)
|
||||
return response.Data("group_page.gotmpl", p)
|
||||
}
|
||||
|
||||
func Group(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
if app.User.IsAdmin {
|
||||
func Group(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
user := app.User()
|
||||
if user.IsAdmin {
|
||||
return AdminGroup(app, h, r)
|
||||
}
|
||||
groupReference := r.PathValue("groupReference")
|
||||
group, err := app.GetGroupByReference(groupReference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err)
|
||||
log.Printf("Couldn't get group: %s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(")
|
||||
}
|
||||
if group == nil {
|
||||
return rsvp.Error(http.StatusNotFound, "Group not found. (It might be because you're not a member)")
|
||||
return response.Error(http.StatusNotFound, "Group not found. (It might be because you're not a member)")
|
||||
}
|
||||
index := group.MemberIndex(app.User.Id)
|
||||
index := group.MemberIndex(user.Id)
|
||||
group.Members = slices.Delete(group.Members, index, index+1)
|
||||
p := GroupProps{
|
||||
Group: group,
|
||||
CurrentUsername: app.User.Name,
|
||||
Navbar: templates.UserNavCollapse(user.Name, user.PasswordFromAdmin),
|
||||
Group: group,
|
||||
}
|
||||
return rsvp.Data("group_page.gotmpl", p)
|
||||
return response.Data("group_page.gotmpl", p)
|
||||
}
|
||||
|
||||
func PublicGroup(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func PublicGroup(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
groupReference := r.PathValue("groupReference")
|
||||
group, err := lishwist.GetGroupByReference(groupReference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err)
|
||||
log.Printf("Couldn't get group: %s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(")
|
||||
}
|
||||
if group == nil {
|
||||
return response.Error(http.StatusNotFound, "Group not found")
|
||||
}
|
||||
p := GroupProps{
|
||||
Group: group,
|
||||
Navbar: templates.DefaultNavCollapse(),
|
||||
Group: group,
|
||||
}
|
||||
return rsvp.Data("public_group_page.gotmpl", p)
|
||||
return response.Data("public_group_page.gotmpl", p)
|
||||
}
|
||||
|
||||
func GroupPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func GroupPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
admin := app.Admin()
|
||||
if admin == nil {
|
||||
return NotFound(h, r)
|
||||
return response.NotFound()
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
form := r.ParseForm()
|
||||
|
||||
var group *lishwist.Group
|
||||
|
||||
reference := r.PathValue("groupReference")
|
||||
name := form.Get("name")
|
||||
addUsers := form["addUser"]
|
||||
removeUsers := form["removeUser"]
|
||||
name := r.Form.Get("name")
|
||||
addUsers := r.Form["addUser"]
|
||||
removeUsers := r.Form["removeUser"]
|
||||
|
||||
if name != "" {
|
||||
createdGroup, err := admin.CreateGroup(name, reference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to create group: %s", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to create group: %s", err)
|
||||
}
|
||||
group = createdGroup
|
||||
} else {
|
||||
existingGroup, err := lishwist.GetGroupByReference(reference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to get group: %s", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get group: %s", err)
|
||||
}
|
||||
if existingGroup == nil {
|
||||
return rsvp.Error(http.StatusNotFound, "Group not found", err)
|
||||
return response.Error(http.StatusNotFound, "Group not found: %s", err)
|
||||
}
|
||||
group = existingGroup
|
||||
|
||||
for _, userId := range removeUsers {
|
||||
index := group.MemberIndex(userId)
|
||||
if index == -1 {
|
||||
return rsvp.Error(http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId)
|
||||
return response.Error(http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId)
|
||||
}
|
||||
err = admin.RemoveUserFromGroup(userId, group.Id)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "On group %q failed to remove user with id %s: %s", reference, userId, err)
|
||||
return response.Error(http.StatusInternalServerError, "On group %q failed to remove user with id %s: %s", reference, userId, err)
|
||||
}
|
||||
group.Members = slices.Delete(group.Members, index, index+1)
|
||||
}
|
||||
|
|
@ -112,31 +129,36 @@ func GroupPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Respo
|
|||
for _, userId := range addUsers {
|
||||
user, err := admin.GetUser(userId)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s could not be fetched: %s", userId, err)
|
||||
return response.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s could not be fetched: %s", userId, err)
|
||||
}
|
||||
if user == nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s does not exist", userId)
|
||||
return response.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s does not exist", userId)
|
||||
}
|
||||
err = admin.AddUserToGroup(user.Id, group.Id)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but failed to add user with id %s: %s", userId, err)
|
||||
return response.Error(http.StatusInternalServerError, "Groups exists, but failed to add user with id %s: %s", userId, err)
|
||||
}
|
||||
group.Members = append(group.Members, *user)
|
||||
}
|
||||
|
||||
return rsvp.Data("", group)
|
||||
return response.Data("", group)
|
||||
}
|
||||
|
||||
func Groups(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
type GroupList struct {
|
||||
XMLName xml.Name `xml:"Groups" json:"-"`
|
||||
Groups []lishwist.Group `xml:"Group"`
|
||||
}
|
||||
|
||||
func Groups(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
admin := app.Admin()
|
||||
if admin == nil {
|
||||
return NotFound(h, r)
|
||||
return response.NotFound()
|
||||
}
|
||||
|
||||
groups, err := admin.ListGroups()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to get groups: %s", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get groups: %s", err)
|
||||
}
|
||||
|
||||
return rsvp.Data("", groups)
|
||||
return response.Data("", GroupList{Groups: groups})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,55 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/env"
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
"lishwist/http/templates"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
type HomeProps struct {
|
||||
Username string
|
||||
Gifts []lishwist.Wish
|
||||
Todo []lishwist.Wish
|
||||
Reference string
|
||||
HostUrl string
|
||||
Groups []lishwist.Group
|
||||
Navbar templates.NavCollapse
|
||||
Gifts []lishwist.Wish
|
||||
Todo []lishwist.Wish
|
||||
Groups []lishwist.Group
|
||||
}
|
||||
|
||||
func Home(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func Home(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
gifts, err := app.GetWishes()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get gifts: %s", err)
|
||||
log.Printf("Failed to get gifts: %s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(")
|
||||
}
|
||||
todo, err := app.GetTodo()
|
||||
user := app.User()
|
||||
todo, err := user.GetTodo()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get todo: %s", err)
|
||||
log.Printf("Failed to get todo: %s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(")
|
||||
}
|
||||
groups, err := app.GetGroups()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get groups: %s", err)
|
||||
log.Printf("Failed to get groups: %s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(")
|
||||
}
|
||||
p := HomeProps{Username: app.User.Name, Gifts: gifts, Todo: todo, Reference: app.User.Reference, HostUrl: env.HostUrl.String(), Groups: groups}
|
||||
return rsvp.Data("home.gotmpl", p)
|
||||
p := HomeProps{Navbar: templates.NavCollapse{
|
||||
User: &templates.User{Name: user.Name, CopyList: &templates.CopyList{Domain: env.Configuration.HostUrl, Reference: user.Reference}},
|
||||
AccountLink: &templates.AccountLink{Alert: user.PasswordFromAdmin},
|
||||
}, Gifts: gifts, Todo: todo, Groups: groups}
|
||||
return response.Data("home.gotmpl", p)
|
||||
}
|
||||
|
||||
func HomePost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
switch form.Get("intent") {
|
||||
func HomePost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
|
||||
switch r.Form.Get("intent") {
|
||||
case "add_idea":
|
||||
return WishlistAdd(app, h, r)
|
||||
case "delete_idea":
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/api"
|
||||
"lishwist/http/rsvp"
|
||||
"net/http"
|
||||
"lishwist/http/response"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func Login(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
session := r.GetSession()
|
||||
|
||||
func Login(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
props := api.NewLoginProps("", "")
|
||||
|
||||
flash := session.FlashGet()
|
||||
flash := s.FlashGet()
|
||||
flashProps, ok := flash.(*api.LoginProps)
|
||||
if ok {
|
||||
props.Username.Value = flashProps.Username.Value
|
||||
|
|
@ -20,50 +24,57 @@ func Login(h http.Header, r *rsvp.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 = session.FlashGet()
|
||||
successfulReg, _ := flash.(bool)
|
||||
if successfulReg {
|
||||
props.SuccessfulRegistration = true
|
||||
}
|
||||
|
||||
return rsvp.Data("login.gotmpl", props).SaveSession(session)
|
||||
return rsvp.Response{TemplateName: "login.gotmpl", Body: props}
|
||||
}
|
||||
|
||||
func LoginPost(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
session := r.GetSession()
|
||||
func LoginPost(session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
|
||||
username := form.Get("username")
|
||||
password := form.Get("password")
|
||||
username = r.Form.Get("username")
|
||||
password = r.Form.Get("password")
|
||||
}
|
||||
|
||||
props := api.NewLoginProps(username, password)
|
||||
|
||||
resp := rsvp.SeeOther(r.URL.Path, props)
|
||||
|
||||
valid := props.Validate()
|
||||
props.Password.Value = ""
|
||||
if !valid {
|
||||
session.FlashSet(&props)
|
||||
return rsvp.SeeOther("/").SaveSession(session).Log("Invalid props: %#v\n", props)
|
||||
log.Printf("Invalid props: %#v\n", props)
|
||||
return resp
|
||||
}
|
||||
|
||||
app, err := lishwist.Login(username, password)
|
||||
appSession, err := lishwist.Login(username, password, time.Hour*24)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case lishwist.ErrorInvalidCredentials:
|
||||
props.GeneralError = "Username or password invalid"
|
||||
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"
|
||||
session.FlashSet(&props)
|
||||
return rsvp.SeeOther("/").SaveSession(session).Log("Invalid credentials: %s: %#v\n", err, props)
|
||||
log.Printf("Invalid credentials: %s: %#v\n", err, props)
|
||||
return resp
|
||||
default:
|
||||
props.GeneralError = "Something went wrong."
|
||||
session.FlashSet(&props)
|
||||
return rsvp.SeeOther("/").SaveSession(session).Log("Login error: %s\n", err)
|
||||
log.Printf("Login error: %s\n", err)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
session.SetID("")
|
||||
session.SetValue("authorized", true)
|
||||
session.SetValue("username", app.User.Name)
|
||||
session.SetValue("sessionKey", appSession.Key)
|
||||
|
||||
return rsvp.SeeOther(r.URL().Path).SaveSession(session)
|
||||
return rsvp.SeeOther(r.URL.Path, "Login successful!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func LogoutPost(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
session := r.GetSession()
|
||||
|
||||
func LogoutPost(session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
session.Options().MaxAge = 0
|
||||
session.ClearValues()
|
||||
|
||||
return rsvp.SeeOther("/").SaveSession(session)
|
||||
return rsvp.SeeOther("/", "Logout successful")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package routing
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func NotFound(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
return rsvp.Error(http.StatusNotFound, "Page not found")
|
||||
func NotFound(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
return response.Error(http.StatusNotFound, "Page not found")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ import (
|
|||
"errors"
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/api"
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func Register(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func Register(session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
props := api.NewRegisterProps("", "", "")
|
||||
|
||||
session := r.GetSession()
|
||||
flash := session.FlashGet()
|
||||
|
||||
flashProps, _ := flash.(*api.RegisterProps)
|
||||
|
|
@ -23,16 +25,24 @@ func Register(h http.Header, r *rsvp.Request) rsvp.Response {
|
|||
props.ConfirmPassword.Error = flashProps.ConfirmPassword.Error
|
||||
}
|
||||
|
||||
return rsvp.Data("register.gotmpl", props).SaveSession(session)
|
||||
return response.Data("register.gotmpl", props)
|
||||
}
|
||||
|
||||
func RegisterPost(h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
s := r.GetSession()
|
||||
func RegisterPost(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
var confirmPassword string
|
||||
username, newPassword, ok := r.BasicAuth()
|
||||
if ok {
|
||||
confirmPassword = newPassword
|
||||
} else {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
|
||||
username := form.Get("username")
|
||||
newPassword := form.Get("newPassword")
|
||||
confirmPassword := form.Get("confirmPassword")
|
||||
username = r.Form.Get("username")
|
||||
newPassword = r.Form.Get("newPassword")
|
||||
confirmPassword = r.Form.Get("confirmPassword")
|
||||
}
|
||||
|
||||
props := api.NewRegisterProps(username, newPassword, confirmPassword)
|
||||
|
||||
|
|
@ -41,7 +51,8 @@ func RegisterPost(h http.Header, r *rsvp.Request) rsvp.Response {
|
|||
props.ConfirmPassword.Value = ""
|
||||
if !valid {
|
||||
s.FlashSet(&props)
|
||||
return rsvp.SeeOther("/").SaveSession(s).Log("Invalid props: %#v\n", props)
|
||||
log.Printf("Invalid register props: %#v\n", props)
|
||||
return rsvp.SeeOther(r.URL.Path, props)
|
||||
}
|
||||
|
||||
_, err := lishwist.Register(username, newPassword)
|
||||
|
|
@ -52,9 +63,10 @@ func RegisterPost(h http.Header, r *rsvp.Request) rsvp.Response {
|
|||
props.GeneralError = "Something went wrong."
|
||||
}
|
||||
s.FlashSet(&props)
|
||||
return rsvp.SeeOther("/register").SaveSession(s).Log("Registration failed: %s\n", err)
|
||||
log.Printf("Registration failed: %s\n", err)
|
||||
return rsvp.SeeOther(r.URL.Path, props)
|
||||
}
|
||||
|
||||
s.FlashSet(true)
|
||||
return rsvp.SeeOther("/").SaveSession(s)
|
||||
s.FlashSet(&api.LoginProps{SuccessfulRegistration: true})
|
||||
return rsvp.SeeOther("/", "Registration successful!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,38 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/rsvp"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/response"
|
||||
)
|
||||
|
||||
func TodoUpdate(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
func TodoUpdate(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")
|
||||
}
|
||||
|
||||
switch form.Get("intent") {
|
||||
switch r.Form.Get("intent") {
|
||||
case "unclaim_todo":
|
||||
unclaims := form["gift"]
|
||||
unclaims := r.Form["gift"]
|
||||
err := app.ClaimWishes([]string{}, unclaims)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err)
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update claim...")
|
||||
}
|
||||
case "complete_todo":
|
||||
claims := form["gift"]
|
||||
claims := r.Form["gift"]
|
||||
err := app.CompleteWishes(claims)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err)
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to complete gifts...")
|
||||
}
|
||||
default:
|
||||
return rsvp.Error(http.StatusBadRequest, "Invalid intent")
|
||||
return response.Error(http.StatusBadRequest, "Invalid intent")
|
||||
}
|
||||
return rsvp.SeeOther("/")
|
||||
return rsvp.SeeOther("/", "Update successful")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,98 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/rsvp"
|
||||
"lishwist/http/response"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func Users(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
type UserList struct {
|
||||
XMLName xml.Name `xml:"Users" json:"-"`
|
||||
Users []lishwist.User `xml:"User"`
|
||||
}
|
||||
|
||||
func Users(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
admin := app.Admin()
|
||||
if admin == nil {
|
||||
return NotFound(h, r)
|
||||
return response.NotFound()
|
||||
}
|
||||
|
||||
users, err := admin.ListUsers()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to get users: %s", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get users: %s", err)
|
||||
}
|
||||
|
||||
return rsvp.Data("", users)
|
||||
return response.Data("", UserList{Users: users})
|
||||
}
|
||||
|
||||
func User(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func User(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
admin := app.Admin()
|
||||
if admin == nil {
|
||||
return NotFound(h, r)
|
||||
return response.NotFound()
|
||||
}
|
||||
|
||||
reference := r.PathValue("userReference")
|
||||
|
||||
user, err := lishwist.GetUserByReference(reference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get user: %s", err)
|
||||
}
|
||||
if user == nil {
|
||||
return rsvp.Error(http.StatusNotFound, "User not found")
|
||||
return response.Error(http.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
return rsvp.Data("", user)
|
||||
return response.Data("", user)
|
||||
}
|
||||
|
||||
func UserPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
func UserPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
admin := app.Admin()
|
||||
if admin == nil {
|
||||
return NotFound(h, r)
|
||||
return response.NotFound()
|
||||
}
|
||||
|
||||
form := r.ParseForm()
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
|
||||
reference := r.PathValue("userReference")
|
||||
if reference == app.User.Reference {
|
||||
return rsvp.Error(http.StatusForbidden, "You cannot delete yourself.")
|
||||
}
|
||||
|
||||
user, err := lishwist.GetUserByReference(reference)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err)
|
||||
}
|
||||
if user == nil {
|
||||
return rsvp.Error(http.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
intent := form.Get("intent")
|
||||
intent := r.Form.Get("intent")
|
||||
|
||||
if intent != "" {
|
||||
err = admin.UserSetLive(reference, intent != "delete")
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to delete user: %s", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return rsvp.Data("", user)
|
||||
user, err := lishwist.GetUserByReference(reference)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get user: %s", err)
|
||||
}
|
||||
|
||||
return response.Data("", user)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,95 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/rsvp"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
|
||||
lishwist "lishwist/core"
|
||||
"lishwist/http/response"
|
||||
)
|
||||
|
||||
func WishlistAdd(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
newGiftName := form.Get("gift_name")
|
||||
err := app.MakeWish(newGiftName)
|
||||
func WishlistAdd(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to add gift.").LogError(err)
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
return rsvp.SeeOther("/")
|
||||
|
||||
newGiftName := r.Form.Get("gift_name")
|
||||
_, err = app.MakeWish(newGiftName)
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to add gift.")
|
||||
}
|
||||
return rsvp.SeeOther("/", "Wish added!")
|
||||
}
|
||||
|
||||
func WishlistDelete(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
targets := form["gift"]
|
||||
err := app.RevokeWishes(targets...)
|
||||
func WishlistDelete(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to remove gifts.").LogError(err)
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
return rsvp.SeeOther("/")
|
||||
|
||||
targets := r.Form["gift"]
|
||||
err = app.RevokeWishes(targets...)
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to remove gifts.")
|
||||
}
|
||||
return rsvp.SeeOther("/", "Wish deleted")
|
||||
}
|
||||
|
||||
func ForeignWishlistPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
|
||||
form := r.ParseForm()
|
||||
func ForeignWishlistPost(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to parse form")
|
||||
}
|
||||
|
||||
userReference := r.PathValue("userReference")
|
||||
intent := form.Get("intent")
|
||||
resp := rsvp.SeeOther("/lists/"+userReference, "Update successful")
|
||||
intent := r.Form.Get("intent")
|
||||
switch intent {
|
||||
case "claim":
|
||||
claims := form["unclaimed"]
|
||||
unclaims := form["claimed"]
|
||||
claims := r.Form["unclaimed"]
|
||||
unclaims := r.Form["claimed"]
|
||||
err := app.ClaimWishes(claims, unclaims)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err)
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update claim...")
|
||||
}
|
||||
resp.Body = "Successfully claimed wishes"
|
||||
case "complete":
|
||||
claims := form["claimed"]
|
||||
claims := r.Form["claimed"]
|
||||
err := app.CompleteWishes(claims)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err)
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to complete gifts...")
|
||||
}
|
||||
resp.Body = "Successfully completed wishes"
|
||||
case "add":
|
||||
wishName := form.Get("gift_name")
|
||||
wishName := r.Form.Get("gift_name")
|
||||
if wishName == "" {
|
||||
return rsvp.Error(http.StatusBadRequest, "Gift name not provided")
|
||||
return response.Error(http.StatusBadRequest, "Gift name not provided")
|
||||
}
|
||||
err := app.SuggestWishForUser(userReference, wishName)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to add gift idea to other user...").LogError(err)
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to add gift idea to other user...")
|
||||
}
|
||||
resp.Body = "Successfully added wishes"
|
||||
case "delete":
|
||||
claims := form["unclaimed"]
|
||||
unclaims := form["claimed"]
|
||||
claims := r.Form["unclaimed"]
|
||||
unclaims := r.Form["claimed"]
|
||||
gifts := append(claims, unclaims...)
|
||||
err := app.RecindWishesForUser(gifts...)
|
||||
if err != nil {
|
||||
return rsvp.Error(http.StatusInternalServerError, "Failed to remove gift idea for other user...").LogError(err)
|
||||
log.Printf("%s\n", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to remove gift idea for other user...")
|
||||
}
|
||||
resp.Body = "Successfully removed wishes"
|
||||
default:
|
||||
return rsvp.Error(http.StatusBadRequest, "Invalid intent %q", intent)
|
||||
return response.Error(http.StatusBadRequest, "Invalid intent %q", intent)
|
||||
}
|
||||
return rsvp.SeeOther("/list/" + userReference)
|
||||
return resp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package rsvp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/Teajey/sqlstore"
|
||||
)
|
||||
|
||||
type ServeMux struct {
|
||||
inner *http.ServeMux
|
||||
store *sqlstore.Store
|
||||
}
|
||||
|
||||
func NewServeMux(store *sqlstore.Store) *ServeMux {
|
||||
return &ServeMux{
|
||||
inner: http.NewServeMux(),
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
m.inner.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
ServeHTTP(h http.Header, r *Request) Response
|
||||
}
|
||||
|
||||
type HandlerFunc func(h http.Header, r *Request) Response
|
||||
|
||||
func (m *ServeMux) HandleFunc(pattern string, handler HandlerFunc) {
|
||||
m.inner.HandleFunc(pattern, func(w http.ResponseWriter, stdReq *http.Request) {
|
||||
r := wrapStdRequest(m.store, stdReq)
|
||||
|
||||
response := handler(w.Header(), &r)
|
||||
|
||||
err := response.Write(w, stdReq)
|
||||
if err != nil {
|
||||
response.Data = struct{ Message error }{err}
|
||||
response.HtmlTemplateName = "error_page.gotmpl"
|
||||
response.Status = http.StatusInternalServerError
|
||||
} else {
|
||||
return
|
||||
}
|
||||
err = response.Write(w, stdReq)
|
||||
if err != nil {
|
||||
log.Printf("Failed to write rsvp.Response to bytes: %s\n", err)
|
||||
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *ServeMux) Handle(pattern string, handler Handler) {
|
||||
m.HandleFunc(pattern, handler.ServeHTTP)
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package rsvp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/Teajey/sqlstore"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
inner *http.Request
|
||||
store *sqlstore.Store
|
||||
}
|
||||
|
||||
func wrapStdRequest(store *sqlstore.Store, r *http.Request) Request {
|
||||
return Request{
|
||||
inner: r,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Request) GetSession() Session {
|
||||
session, _ := r.store.Get(r.inner, "lishwist_user")
|
||||
return Session{session}
|
||||
}
|
||||
|
||||
func (r *Request) ParseForm() url.Values {
|
||||
err := r.inner.ParseForm()
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse form: %s\n", err)
|
||||
}
|
||||
return r.inner.Form
|
||||
}
|
||||
|
||||
func (r *Request) PathValue(name string) string {
|
||||
return r.inner.PathValue(name)
|
||||
}
|
||||
|
||||
func (r *Request) URL() *url.URL {
|
||||
return r.inner.URL
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
package rsvp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"lishwist/http/templates"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
HtmlTemplateName string
|
||||
Data any
|
||||
SeeOther string
|
||||
Session *Session
|
||||
Status int
|
||||
LogMessage string
|
||||
}
|
||||
|
||||
func (res *Response) Write(w http.ResponseWriter, r *http.Request) error {
|
||||
if res.LogMessage != "" {
|
||||
log.Printf("%s --- %s\n", res.Data, res.LogMessage)
|
||||
}
|
||||
|
||||
if res.Session != nil {
|
||||
err := res.Session.inner.Save(r, w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to write session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if res.SeeOther != "" {
|
||||
http.Redirect(w, r, res.SeeOther, http.StatusSeeOther)
|
||||
if res.Session != nil {
|
||||
flash := res.Session.FlashPeek()
|
||||
if flash != nil {
|
||||
err := json.NewEncoder(w).Encode(flash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
bodyBytes := bytes.NewBuffer([]byte{})
|
||||
accept := r.Header.Get("Accept")
|
||||
|
||||
if res.Status != 0 {
|
||||
w.WriteHeader(res.Status)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(accept, "text/html"):
|
||||
if res.HtmlTemplateName == "" {
|
||||
err := json.NewEncoder(bodyBytes).Encode(res.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := templates.Execute(bodyBytes, res.HtmlTemplateName, res.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case strings.Contains(accept, "application/json"):
|
||||
err := json.NewEncoder(bodyBytes).Encode(res.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
err := json.NewEncoder(bodyBytes).Encode(res.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := w.Write(bodyBytes.Bytes())
|
||||
if err != nil {
|
||||
log.Printf("Failed to write rsvp.Response to HTTP: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Data(htmlTemplateName string, data any) Response {
|
||||
return Response{
|
||||
HtmlTemplateName: htmlTemplateName,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Response) Log(format string, a ...any) Response {
|
||||
r.LogMessage = fmt.Sprintf(format, a...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Response) LogError(err error) Response {
|
||||
r.LogMessage = fmt.Sprintf("%s", err)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Response) SaveSession(s Session) Response {
|
||||
r.Session = &s
|
||||
return r
|
||||
}
|
||||
|
||||
func SeeOther(url string) Response {
|
||||
return Response{SeeOther: url}
|
||||
}
|
||||
|
||||
func Error(status int, format string, a ...any) Response {
|
||||
return Response{
|
||||
Status: status,
|
||||
HtmlTemplateName: "error_page.gotmpl",
|
||||
Data: struct{ Message string }{fmt.Sprintf(format, a...)},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"lishwist/http/api"
|
||||
"lishwist/http/env"
|
||||
"lishwist/http/response"
|
||||
"lishwist/http/router"
|
||||
"lishwist/http/routing"
|
||||
"lishwist/http/session"
|
||||
|
||||
"github.com/Teajey/rsvp"
|
||||
)
|
||||
|
||||
func prefixMovedPermanently(before, after string) response.HandlerFunc {
|
||||
return func(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
suffix := strings.TrimPrefix(r.RequestURI, before)
|
||||
return rsvp.MovedPermanently(after + suffix)
|
||||
}
|
||||
}
|
||||
|
||||
func prefixPermanentRedirect(before, after string) response.HandlerFunc {
|
||||
return func(s *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||
suffix := strings.TrimPrefix(r.RequestURI, before)
|
||||
return rsvp.PermanentRedirect(after + suffix)
|
||||
}
|
||||
}
|
||||
|
||||
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.Options.MaxAge = 86_400 // 24 hours in seconds
|
||||
store.Options.Secure = useSecureCookies
|
||||
store.Options.HttpOnly = true
|
||||
store.Options.Path = "/"
|
||||
store.Options.SameSite = http.SameSiteLaxMode
|
||||
|
||||
r := router.New(store)
|
||||
|
||||
r.Public.HandleFunc("GET /", routing.Login)
|
||||
r.Public.HandleFunc("GET /groups/{groupReference}", routing.PublicGroup)
|
||||
r.Public.HandleFunc("GET /lists/{userReference}", routing.PublicWishlist)
|
||||
r.Public.HandleFunc("GET /register", routing.Register)
|
||||
r.Public.HandleFunc("POST /", routing.LoginPost)
|
||||
r.Public.HandleFunc("POST /register", routing.RegisterPost)
|
||||
|
||||
r.Private.HandleFunc("GET /events", routing.ExpectAppSession(routing.EventList))
|
||||
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))
|
||||
r.Private.HandleFunc("GET /lists/{userReference}", routing.ExpectAppSession(routing.ForeignWishlist))
|
||||
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)
|
||||
r.Private.HandleFunc("POST /users/{userReference}", routing.ExpectAppSession(routing.UserPost))
|
||||
r.Private.HandleFunc("POST /{$}", routing.ExpectAppSession(routing.HomePost))
|
||||
|
||||
// Deprecated
|
||||
r.HandleFunc("GET /group/{groupReference}", prefixMovedPermanently("/group/", "/groups/"))
|
||||
r.HandleFunc("GET /list/{groupReference}", prefixMovedPermanently("/list/", "/lists/"))
|
||||
r.HandleFunc("POST /group/{groupReference}", prefixPermanentRedirect("/group/", "/groups/"))
|
||||
r.HandleFunc("POST /list/{groupReference}", prefixPermanentRedirect("/list/", "/lists/"))
|
||||
|
||||
return r
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"lishwist/http/internal/id"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
var inMemStore = make(map[string]string)
|
||||
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
func NewInMemoryStore(keyPairs ...[]byte) *Store {
|
||||
return &Store{
|
||||
callbacks: Callbacks{
|
||||
Delete: func(key string) error {
|
||||
delete(inMemStore, key)
|
||||
return nil
|
||||
},
|
||||
Insert: func(encodedValues string) (string, error) {
|
||||
key := id.Generate()
|
||||
inMemStore[key] = encodedValues
|
||||
return key, nil
|
||||
},
|
||||
Select: func(key string) (string, error) {
|
||||
encodedValues, ok := inMemStore[key]
|
||||
if !ok {
|
||||
return "", errNotFound
|
||||
}
|
||||
return encodedValues, nil
|
||||
},
|
||||
Update: func(key string, encodedValues string) error {
|
||||
inMemStore[key] = encodedValues
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||
Options: &sessions.Options{},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package sesh
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
func GetFirstFlash(w http.ResponseWriter, r *http.Request, session *sessions.Session, key ...string) (any, error) {
|
||||
flashes := session.Flashes(key...)
|
||||
|
||||
if len(flashes) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
flash := flashes[0]
|
||||
|
||||
if err := session.Save(r, w); err != nil {
|
||||
log.Println("Couldn't save session:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return flash, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Callbacks struct {
|
||||
Delete func(id string) error
|
||||
Insert func(encodedValues string) (string, error)
|
||||
Select func(id string) (string, error)
|
||||
Update func(id, encodedValues string) error
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
callbacks Callbacks
|
||||
Codecs []securecookie.Codec
|
||||
Options *sessions.Options
|
||||
}
|
||||
|
||||
func NewGenericStore(cb Callbacks, keyPairs ...[]byte) *Store {
|
||||
return &Store{
|
||||
callbacks: cb,
|
||||
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||
Options: &sessions.Options{},
|
||||
}
|
||||
}
|
||||
|
||||
// Get should return a cached session.
|
||||
func (m *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||
return sessions.GetRegistry(r).Get(m, name)
|
||||
}
|
||||
|
||||
// New should create and return a new session.
|
||||
//
|
||||
// Note that New should never return a nil session, even in the case of
|
||||
// an error if using the Registry infrastructure to cache the session.
|
||||
func (s *Store) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||
session := sessions.NewSession(s, name)
|
||||
opts := *s.Options
|
||||
session.Options = &opts
|
||||
session.IsNew = true
|
||||
|
||||
var err error
|
||||
|
||||
c, errCookie := r.Cookie(name)
|
||||
if errCookie != nil {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
|
||||
if err != nil {
|
||||
return session, fmt.Errorf("failed to decode session id: %w", err)
|
||||
}
|
||||
|
||||
sessionValue, err := s.callbacks.Select(session.ID)
|
||||
if err != nil {
|
||||
return session, fmt.Errorf("failed to get session value: %w", err)
|
||||
}
|
||||
|
||||
err = securecookie.DecodeMulti(name, string(sessionValue), &session.Values, s.Codecs...)
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
} else {
|
||||
err = fmt.Errorf("failed to decode session values: %w", err)
|
||||
}
|
||||
|
||||
return session, err
|
||||
}
|
||||
|
||||
// Save should persist session to the underlying store implementation.
|
||||
func (s *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||
// Delete if max-age is <= 0
|
||||
if session.Options.MaxAge <= 0 {
|
||||
err := s.callbacks.Delete(session.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete session: %w", err)
|
||||
}
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
encodedValues, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||
s.Codecs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode cookie value: %w", err)
|
||||
}
|
||||
|
||||
if session.ID == "" {
|
||||
i, err := s.callbacks.Insert(encodedValues)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert session: %w", err)
|
||||
}
|
||||
|
||||
session.ID = i
|
||||
} else {
|
||||
err := s.callbacks.Update(session.ID, encodedValues)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
encodedId, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
||||
s.Codecs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode cookie value: %w", err)
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), encodedId, session.Options))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
{{template "head" .}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<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 the admin doesn'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>
|
||||
|
|
@ -12,10 +12,7 @@
|
|||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
{{define "head"}}
|
||||
<title>Lishwist</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
|
|
@ -42,24 +39,66 @@
|
|||
submitter.disabled = !accepted;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
{{end}}
|
||||
|
||||
<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">
|
||||
{{template "navbar" .}}
|
||||
</div>
|
||||
{{define "navbar"}}
|
||||
<div class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-brand">Lishwist</div>
|
||||
{{with .}}
|
||||
<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 and .AccountLink .AccountLink.Alert}} <span
|
||||
class="badge text-bg-danger">!</span>{{end}}
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarToggle">
|
||||
{{with .Links}}
|
||||
<nav>
|
||||
<ul class="navbar-nav">
|
||||
{{range .}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Href}}">{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<div class="flex-grow-1"></div>
|
||||
{{with .User}}
|
||||
<ul class="navbar-nav">
|
||||
{{with .CopyList}}
|
||||
<li class="nav-item"><button class="btn btn-success"
|
||||
onclick="navigator.clipboard.writeText('{{.Domain}}/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>
|
||||
{{end}}
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<button class="nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Logged in as '{{.Name}}'{{if and $.AccountLink $.AccountLink.Alert}} <span class="badge text-bg-danger">!</span>{{end}}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{{with $.AccountLink}}
|
||||
<li>
|
||||
<a class="dropdown-item" href="/account">Account{{if .Alert}} <span
|
||||
class="badge text-bg-danger">!</span>{{end}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<form class="d-contents" method="post" action="/logout">
|
||||
<button class="dropdown-item" type="submit">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
</body>
|
||||
{{end}}
|
||||
|
||||
</html>
|
||||
{{define "login_prompt"}}
|
||||
<a href="/">Login</a> or <a href="/register">register</a>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
{{define "navbar"}}
|
||||
<nav>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "body"}}
|
||||
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p class="mb-0">{{.Message}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p class="mb-0">{{.Message}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,98 +1,81 @@
|
|||
{{define "navbar"}}
|
||||
<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>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "body"}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2>{{.Username}}'s list</h2>
|
||||
{{with .Gifts}}
|
||||
<form method="post" autocomplete="off"
|
||||
onchange="acceptNames(this, 'claimSubmit', 'claimed', 'unclaimed'); acceptNames(this, 'completeSubmit', 'claimed'); acceptAttribute(this, 'deleteSubmit', 'data-deletable')">
|
||||
<ul class="list-group mb-3">
|
||||
{{range .}}
|
||||
{{$isMine := eq .ClaimantId $.CurrentUserId}}
|
||||
{{$createdByMe := eq .CreatorId $.CurrentUserId}}
|
||||
{{$outsideIdea := ne .RecipientId .CreatorId}}
|
||||
<li class="list-group-item{{if .Sent}} list-group-item-light{{end}}">
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2>{{.Username}}'s list</h2>
|
||||
{{with .Gifts}}
|
||||
<form method="post" autocomplete="off"
|
||||
onchange="acceptNames(this, 'claimSubmit', 'claimed', 'unclaimed'); acceptNames(this, 'completeSubmit', 'claimed'); acceptAttribute(this, 'deleteSubmit', 'data-deletable')">
|
||||
<ul class="list-group mb-3">
|
||||
{{range .}}
|
||||
{{$isMine := eq .ClaimantId $.CurrentUserId}}
|
||||
{{$createdByMe := eq .CreatorId $.CurrentUserId}}
|
||||
{{$outsideIdea := ne .RecipientId .CreatorId}}
|
||||
<li class="list-group-item{{if .Sent}} list-group-item-light{{end}}">
|
||||
|
||||
<input id="foreignlist_select_{{.Id}}" class="form-check-input" type="checkbox"
|
||||
aria-describedby="wish_detail_{{.Id}}" {{if $isMine}} name="claimed" value="{{.Id}}" {{else if
|
||||
.ClaimantId}} disabled {{else}} name="unclaimed" value="{{.Id}}" {{end}} {{if .Sent}} disabled {{end}}
|
||||
{{if $createdByMe}}data-deletable{{end}}>
|
||||
<input id="foreignlist_select_{{.Id}}" class="form-check-input" type="checkbox"
|
||||
aria-describedby="wish_detail_{{.Id}}" {{if $isMine}} name="claimed" value="{{.Id}}" {{else if
|
||||
.ClaimantId}} disabled {{else}} name="unclaimed" value="{{.Id}}" {{end}} {{if .Sent}} disabled {{end}}
|
||||
{{if $createdByMe}}data-deletable{{end}}>
|
||||
|
||||
<label class="form-check-label stretched-link" for="foreignlist_select_{{.Id}}">
|
||||
{{if .Sent}}
|
||||
<s>{{.Name}}</s>
|
||||
{{else}}
|
||||
{{.Name}}
|
||||
{{end}}
|
||||
</label>
|
||||
<label class="form-check-label stretched-link" for="foreignlist_select_{{.Id}}">
|
||||
{{if .Sent}}
|
||||
<s>{{.Name}}</s>
|
||||
{{else}}
|
||||
{{.Name}}
|
||||
{{end}}
|
||||
</label>
|
||||
|
||||
{{if or .ClaimantId $outsideIdea}}
|
||||
<div class="d-inline" id="wish_detail_{{.Id}}">
|
||||
{{if .ClaimantId}}
|
||||
<span style="color: {{if $isMine}}blue{{else}}red{{end}};">{{if .Sent}}Completed{{else}}Claimed{{end}}
|
||||
by
|
||||
{{if $isMine}}<em>you</em>{{else}}{{.ClaimantName}}{{end}}</span>
|
||||
{{end}}
|
||||
{{if or .ClaimantId $outsideIdea}}
|
||||
<div class="d-inline" id="wish_detail_{{.Id}}">
|
||||
{{if .ClaimantId}}
|
||||
<span style="color: {{if $isMine}}blue{{else}}red{{end}};">{{if .Sent}}Completed{{else}}Claimed{{end}}
|
||||
by
|
||||
{{if $isMine}}<em>you</em>{{else}}{{.ClaimantName}}{{end}}</span>
|
||||
{{end}}
|
||||
|
||||
{{if $outsideIdea}}
|
||||
<span style="color: green;">Added by {{if
|
||||
$createdByMe}}<em>you</em>{{else}}{{.CreatorName}}{{end}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if $outsideIdea}}
|
||||
<span style="color: green;">Added by {{if
|
||||
$createdByMe}}<em>you</em>{{else}}{{.CreatorName}}{{end}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<button id="claimSubmit" class="btn btn-warning" type="submit" name="intent" value="claim"
|
||||
disabled>Claim/Unclaim</button>
|
||||
<button id="completeSubmit" class="btn btn-success" type="submit" name="intent" value="complete"
|
||||
disabled>Complete</button>
|
||||
<button id="deleteSubmit" class="btn btn-danger" type="submit" name="intent" value="delete"
|
||||
disabled>Delete</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<p>They don't have any gift ideas. Ask them to think of something, or add an idea yourself! 👇 (everyone
|
||||
except them will be able to see it and claim it)</p>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<button id="claimSubmit" class="btn btn-warning" type="submit" name="intent" value="claim"
|
||||
disabled>Claim/Unclaim</button>
|
||||
<button id="completeSubmit" class="btn btn-success" type="submit" name="intent" value="complete"
|
||||
disabled>Complete</button>
|
||||
<button id="deleteSubmit" class="btn btn-danger" type="submit" name="intent" value="delete"
|
||||
disabled>Delete</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<p>They don't have any gift ideas. Ask them to think of something, or add an idea yourself! 👇 (everyone
|
||||
except them will be able to see it and claim it)</p>
|
||||
{{end}}
|
||||
<form method="post">
|
||||
<div class="input-group mt-3">
|
||||
<input class="form-control" name="gift_name" required aria-describedby="gift_name_help" placeholder="Write a gift idea here" autofocus>
|
||||
<button class="btn btn-primary" type="submit" name="intent" value="add">Add gift idea</button>
|
||||
</div>
|
||||
<div id="gift_name_help" class="form-text">This will be invisible to {{.Username}}, but everyone else will be
|
||||
able to see it and
|
||||
possibly claim it.</div>
|
||||
</form>
|
||||
<form method="post">
|
||||
<div class="input-group mt-3">
|
||||
<input class="form-control" name="gift_name" required aria-describedby="gift_name_help" placeholder="Write a gift idea here" autofocus>
|
||||
<button class="btn btn-primary" type="submit" name="intent" value="add">Add gift idea</button>
|
||||
</div>
|
||||
<div id="gift_name_help" class="form-text">This will be invisible to {{.Username}}, but everyone else will be
|
||||
able to see it and
|
||||
possibly claim it.</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,50 +1,32 @@
|
|||
{{define "navbar"}}
|
||||
<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>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "body"}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2><em>{{.Group.Name}}</em> group members</h2>
|
||||
{{with .Group.Members}}
|
||||
<ul class="list-group">
|
||||
{{range .}}
|
||||
<li class="list-group-item">
|
||||
<a href="/list/{{.Reference}}">{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>There's nobody else in this group.</p>
|
||||
{{end}}
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2><em>{{.Group.Name}}</em> group members</h2>
|
||||
{{with .Group.Members}}
|
||||
<ul class="list-group">
|
||||
{{range .}}
|
||||
<li class="list-group-item">
|
||||
<a href="/lists/{{.Reference}}">{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>There's nobody else in this group.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,113 +1,103 @@
|
|||
{{define "navbar"}}
|
||||
<div class="flex-grow-1"></div>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><button class="btn btn-success"
|
||||
onclick="navigator.clipboard.writeText('{{.HostUrl}}/list/{{.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>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
{{define "body"}}
|
||||
<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>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<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}}
|
||||
</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>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</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="/list/{{.RecipientRef}}">{{.RecipientName}}</a>
|
||||
</span>
|
||||
</li>
|
||||
<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}}
|
||||
</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>
|
||||
</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>
|
||||
<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}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>You don't belong to any groups</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,45 @@
|
|||
{{define "navbar"}}
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "body"}}
|
||||
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
|
||||
{{if .SuccessfulRegistration}}
|
||||
<div class="alert alert-success" role="alert">
|
||||
<p class="mb-0">Registration successful. Now you can login.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{with .GeneralError}}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p class="mb-0">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="post">
|
||||
<div class="d-flex flex-column">
|
||||
<label>
|
||||
Username
|
||||
{{template "input" .Username}}
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
{{template "input" .Password}}
|
||||
</label>
|
||||
<div class="mb-3">
|
||||
<a href="/register">Register</a>
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar"}}
|
||||
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
|
||||
{{if .SuccessfulRegistration}}
|
||||
<div class="alert alert-success" role="alert">
|
||||
<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>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="post">
|
||||
<div class="d-flex flex-column">
|
||||
<label>
|
||||
Username
|
||||
{{template "input" .Username}}
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
{{template "input" .Password}}
|
||||
</label>
|
||||
<div class="mb-3">
|
||||
<a href="/register">Register</a>
|
||||
</div>
|
||||
<input class="btn btn-primary" type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<input class="btn btn-primary" type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,33 @@
|
|||
{{define "navbar"}}
|
||||
<nav>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "login_prompt"}}
|
||||
<a href="/">Login</a> or <a href="/register">register</a>
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2>{{.Username}}'s list</h2>
|
||||
{{if eq .GiftCount 0}}
|
||||
<p>{{.Username}} hasn't written any gift ideas!</p>
|
||||
<p>{{template "login_prompt"}} to add some! :^)</p>
|
||||
{{else}}
|
||||
{{if eq .GiftCount 1}}
|
||||
<p>{{.Username}} has only written one gift idea.</p>
|
||||
<p>{{template "login_prompt"}} to claim it, or add more! :^)</p>
|
||||
{{else}}
|
||||
<p>{{.Username}} has written {{.GiftCount}} gift ideas.</p>
|
||||
<p>{{template "login_prompt"}} to claim an idea, or add more! :^)</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2>{{.Username}}'s list</h2>
|
||||
{{if eq .GiftCount 0}}
|
||||
<p>{{.Username}} hasn't written any gift ideas!</p>
|
||||
<p>{{template "login_prompt"}} to add some! :^)</p>
|
||||
{{else}}
|
||||
{{if eq .GiftCount 1}}
|
||||
<p>{{.Username}} has only written one gift idea.</p>
|
||||
<p>{{template "login_prompt"}} to claim it, or add more! :^)</p>
|
||||
{{else}}
|
||||
<p>{{.Username}} has written {{.GiftCount}} gift ideas.</p>
|
||||
<p>{{template "login_prompt"}} to claim an idea, or add more! :^)</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,33 @@
|
|||
{{define "navbar"}}
|
||||
<nav>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "login_prompt"}}
|
||||
<a href="/">Login</a> or <a href="/register">register</a>
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2><em>{{.Group.Name}}</em> group members</h2>
|
||||
<p>{{template "login_prompt"}} to see your groups</p>
|
||||
{{with .Group.Members}}
|
||||
<ul class="list-group">
|
||||
{{range .}}
|
||||
<li class="list-group-item">
|
||||
{{.Name}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>There's nobody else in this group.</p>
|
||||
{{end}}
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<div class="overflow-y-scroll flex-grow-1">
|
||||
<div class="container py-5">
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<h2><em>{{.Group.Name}}</em> group members</h2>
|
||||
<p>{{template "login_prompt"}} to see your groups</p>
|
||||
{{with .Group.Members}}
|
||||
<ul class="list-group">
|
||||
{{range .}}
|
||||
<li class="list-group-item">
|
||||
{{.Name}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>There's nobody else in this group.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,40 +1,40 @@
|
|||
{{define "navbar"}}
|
||||
<nav>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{template "head"}}
|
||||
</head>
|
||||
|
||||
{{define "body"}}
|
||||
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>Your password will be stored in a safe, responsible manner; but don't trust my programming skills!</p>
|
||||
<p class="mb-0">Maybe use a password here that you don't use for important things...</p>
|
||||
</div>
|
||||
{{with .GeneralError}}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p class="mb-0">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="post">
|
||||
<div class="d-flex flex-column">
|
||||
<label>
|
||||
Username
|
||||
{{template "input" .Username}}
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
{{template "input" .Password}}
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
{{template "input" .ConfirmPassword}}
|
||||
</label>
|
||||
<input class="btn btn-primary" type="submit" value="Register">
|
||||
<body>
|
||||
<div style="height: 100svh;" class="d-flex flex-column">
|
||||
{{template "navbar" .Navbar}}
|
||||
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>Your password will be stored in a safe, responsible manner; but don't trust my programming skills!</p>
|
||||
<p class="mb-0">Maybe use a password here that you don't use for important things...</p>
|
||||
</div>
|
||||
{{with .GeneralError}}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p class="mb-0">{{.}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="post">
|
||||
<div class="d-flex flex-column">
|
||||
<label>
|
||||
Username
|
||||
{{template "input" .Username}}
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
{{template "input" .Password}}
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
{{template "input" .ConfirmPassword}}
|
||||
</label>
|
||||
<input class="btn btn-primary" type="submit" value="Register">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,46 @@ package templates
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/template"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type AccountLink struct {
|
||||
Alert bool
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Href string
|
||||
Name string
|
||||
}
|
||||
|
||||
type CopyList struct {
|
||||
Domain string
|
||||
Reference string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
CopyList *CopyList
|
||||
}
|
||||
|
||||
type NavCollapse struct {
|
||||
User *User
|
||||
AccountLink *AccountLink
|
||||
Links []Link
|
||||
}
|
||||
|
||||
func DefaultNavCollapse() NavCollapse {
|
||||
return NavCollapse{Links: []Link{{Href: "/", Name: "Home"}}}
|
||||
}
|
||||
|
||||
func UserNavCollapse(username string, accountAlert bool) NavCollapse {
|
||||
return NavCollapse{
|
||||
Links: []Link{{Href: "/", Name: "Home"}},
|
||||
User: &User{Name: username},
|
||||
AccountLink: &AccountLink{Alert: accountAlert},
|
||||
}
|
||||
}
|
||||
|
||||
type InputProps struct {
|
||||
Type string `json:",omitempty"`
|
||||
Name string
|
||||
|
|
@ -30,33 +66,13 @@ func (p *InputProps) Validate() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
var tmpls map[string]*template.Template = loadTemplates()
|
||||
var Template *template.Template
|
||||
|
||||
func Execute(w io.Writer, name string, data any) error {
|
||||
err := tmpls[name].Execute(w, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to execute '%s' template: %w\n", name, err)
|
||||
}
|
||||
return nil
|
||||
func init() {
|
||||
Template = load()
|
||||
}
|
||||
|
||||
func loadTemplates() map[string]*template.Template {
|
||||
homeTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/home.gotmpl"))
|
||||
loginTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/login.gotmpl"))
|
||||
registerTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/register.gotmpl"))
|
||||
foreignTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/foreign_wishlist.gotmpl"))
|
||||
publicWishlistTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/public_foreign_wishlist.gotmpl"))
|
||||
errorTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/error_page.gotmpl"))
|
||||
groupTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/group_page.gotmpl"))
|
||||
publicGroupTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/public_group_page.gotmpl"))
|
||||
return map[string]*template.Template{
|
||||
"home.gotmpl": homeTmpl,
|
||||
"login.gotmpl": loginTmpl,
|
||||
"register.gotmpl": registerTmpl,
|
||||
"foreign_wishlist.gotmpl": foreignTmpl,
|
||||
"public_foreign_wishlist.gotmpl": publicWishlistTmpl,
|
||||
"error_page.gotmpl": errorTmpl,
|
||||
"group_page.gotmpl": groupTmpl,
|
||||
"public_group_page.gotmpl": publicGroupTmpl,
|
||||
}
|
||||
func load() *template.Template {
|
||||
t := template.Must(template.ParseGlob("templates/*.gotmpl"))
|
||||
return t
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -x
|
||||
|
||||
git diff --exit-code $1 api.snap.txt $(find . -name '*_test.go' -type f)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
go doc -all $1 | ./scripts/strip_godoc_comments
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo $(git rev-parse HEAD)$(test -n "$(git status --porcelain)" && echo "*")
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
APISNAP=api.snap.txt
|
||||
|
||||
./scripts/api_snapshot ./core > core/$APISNAP
|
||||
git diff --quiet core/$APISNAP
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "There are unstaged changes to $APISNAP"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import fileinput
|
||||
from collections import deque
|
||||
from typing import Iterator
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def window(it: Iterator[T], size: int = 2) -> Iterator[list[T]]:
|
||||
if size < 2:
|
||||
raise ValueError("Window size must be at least 2")
|
||||
|
||||
window: deque[T] = deque(maxlen=size)
|
||||
|
||||
for _ in range(size):
|
||||
try:
|
||||
window.append(next(it))
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
yield list(window)
|
||||
|
||||
for item in it:
|
||||
window.append(item)
|
||||
yield list(window)
|
||||
|
||||
|
||||
struct = False
|
||||
func = False
|
||||
for line, next_line in window(fileinput.input()):
|
||||
line = line.removesuffix("\n")
|
||||
|
||||
if line.startswith("type ") and line.endswith(" struct {"):
|
||||
struct = True
|
||||
elif struct and line.endswith("}"):
|
||||
struct = False
|
||||
elif line.startswith("func"):
|
||||
func = True
|
||||
elif next_line[:4].strip() != "":
|
||||
func = False
|
||||
|
||||
if struct:
|
||||
if (
|
||||
line != ""
|
||||
and not line.lstrip().startswith("//")
|
||||
or "Has unexported fields" in line
|
||||
):
|
||||
print(line)
|
||||
elif func:
|
||||
if line != "" and not line.startswith(" "):
|
||||
print(line)
|
||||
else:
|
||||
print(line)
|
||||
Loading…
Reference in New Issue