Added users. And forgot I was using git :/

This commit is contained in:
Cameron Reed 2024-09-23 12:50:21 -06:00
parent e96bbdb0dd
commit c9fe62e7fd
23 changed files with 1192 additions and 305 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
test.db
user_dbs/
todo-web

View File

@ -2,16 +2,18 @@ package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/Cameron-Reed1/todo-web/db"
// "github.com/Cameron-Reed1/todo-web/db"
"github.com/Cameron-Reed1/todo-web/types"
)
func GetAll(w http.ResponseWriter, r *http.Request) {
todos, err := db.GetAllTodos()
// todos, err := db.GetAllTodos()
todos, err := []types.Todo(nil), errors.New("Broken right now :/")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("{\"error\":\"Failed to get items\"}"))
@ -44,7 +46,8 @@ func GetTodo(w http.ResponseWriter, r *http.Request) {
return
}
todo, err := db.GetTodo(id)
// todo, err := db.GetTodo(id)
todo, err := types.Todo{Id: int64(id)}, errors.New("Broken right now :/")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("{\"error\":\"No item for id\"}"))
@ -79,7 +82,8 @@ func AddTodo(w http.ResponseWriter, r *http.Request) {
return
}
err = db.AddTodo(&todo)
// err = db.AddTodo(&todo)
err = errors.New("Broken right now :/")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("{\"error\":\"Failed to add item\"}"))

133
auth/auth.go Normal file
View File

@ -0,0 +1,133 @@
package auth
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/Cameron-Reed1/todo-web/types"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/scrypt"
)
var algorithm argon2idHasher = argon2idHasher{
hashLen: 64,
saltLen: 32,
time: 6,
memory: 24 * 1024,
threads: 1,
}
func Hash(password, salt []byte) (*HashSalt, error) {
return algorithm.Hash(password, salt)
}
func Validate(hash, salt, password []byte) bool {
return algorithm.Validate(hash, salt, password)
}
func CreateSessionFor(user_id int64) (*types.Session, error) {
buf := make([]byte, 32)
_, err := rand.Read(buf)
if err != nil {
return nil, err
}
return &types.Session{ SessionId: base64.StdEncoding.EncodeToString(buf), UserId: user_id }, nil
}
func generateSalt(length uint) ([]byte, error) {
salt := make([]byte, length)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
return salt, nil
}
type HashSalt struct {
Hash []byte
Salt []byte
}
type hashAlgo interface {
Hash(password, salt []byte) ([]byte, error)
Validate(hash, salt, password []byte) bool
}
type scryptHasher struct {
hashLen int
saltLen uint
cost int
blockSize int
parallelism int
}
type argon2idHasher struct {
hashLen uint32
saltLen uint
time uint32
memory uint32
threads uint8
}
func (s *scryptHasher) Hash(password, salt []byte) (*HashSalt, error) {
var err error
if salt == nil || len(salt) == 0 {
salt, err = generateSalt(s.saltLen)
if err != nil {
fmt.Println("\x1b[31mError: Failed to generate a password salt\x1b[0m")
return nil, err
}
}
hash, err := scrypt.Key(password, salt, s.cost, s.blockSize, s.parallelism, s.hashLen)
if err != nil {
return nil, err
}
return &HashSalt{Hash: hash, Salt: salt}, nil
}
func (s *scryptHasher) Validate(hash, salt, password []byte) bool {
hashed_password, err := s.Hash(password, salt)
if err != nil {
fmt.Println("\x1b[31mError: Failed to generate a password hash\x1b[0m")
return false
}
return bytes.Equal(hash, hashed_password.Hash)
}
func (a *argon2idHasher) Hash(password, salt []byte) (*HashSalt, error) {
var err error
if salt == nil || len(salt) == 0 {
salt, err = generateSalt(a.saltLen)
if err != nil {
fmt.Println("\x1b[31mError: Failed to generate a password salt\x1b[0m")
return nil, err
}
}
hash := argon2.IDKey(password, salt, a.time, a.memory, a.threads, a.hashLen)
return &HashSalt{Hash: hash, Salt: salt}, nil
}
func (s *argon2idHasher) Validate(hash, salt, password []byte) bool {
hashed_password, err := s.Hash(password, salt)
if err != nil {
fmt.Println("\x1b[31mError: Failed to generate a password hash\x1b[0m")
return false
}
return bytes.Equal(hash, hashed_password.Hash)
}

210
db/db.go
View File

@ -2,220 +2,10 @@ package db
import (
"database/sql"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/Cameron-Reed1/todo-web/types"
)
var db *sql.DB
func Open(path string) {
if db != nil {
log.Fatal("Cannot init DB twice!")
}
var err error
db, err = sql.Open("sqlite3", path)
if err != nil {
log.Fatal(err)
}
err = db.Ping()
if err != nil {
log.Fatal(err)
}
query := `
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start INTEGER,
due INTEGER,
text TEXT NOT NULL,
completed INTEGER NOT NULL
);`
_, err = db.Exec(query)
}
func AddTodo(todo *types.Todo) error {
res, err := db.Exec("INSERT INTO items(start, due, text, completed) values(?, ?, ?, 0)", toNullInt64(todo.Start), toNullInt64(todo.Due), todo.Text)
if err != nil {
return err
}
todo.Id, err = res.LastInsertId()
if err != nil {
return err
}
return nil
}
func GetTodo(id int) (types.Todo, error) {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
row := db.QueryRow("SELECT * FROM items WHERE id=?", id)
err := row.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
return todo, err
}
func GetAllTodos() ([]types.Todo, error) {
var todos []types.Todo
rows, err := db.Query("SELECT * FROM items")
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func GetOverdueTodos() ([]types.Todo, error) {
var todos []types.Todo
rows, err := db.Query("SELECT * FROM items WHERE due < ? AND due IS NOT NULL ORDER BY completed, due", time.Now().Unix())
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func GetTodayTodos() ([]types.Todo, error) {
var todos []types.Todo
now := time.Now()
year, month, day := now.Date()
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
rows, err := db.Query("SELECT * FROM items WHERE (start <= ? OR start IS NULL) AND (due >= ? OR due IS NULL) ORDER BY completed, due NULLS LAST", today.Unix(), now.Unix())
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func GetUpcomingTodos() ([]types.Todo, error) {
var todos []types.Todo
year, month, day := time.Now().Date()
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
rows, err := db.Query("SELECT * FROM items WHERE start > ? ORDER BY completed, start", today.Unix())
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func UpdateTodo(newValues types.Todo) error {
_, err := db.Exec("UPDATE items SET start=?, due=?, text=? WHERE id=?", toNullInt64(newValues.Start), toNullInt64(newValues.Due), newValues.Text, newValues.Id)
return err;
}
func SetCompleted(id int, completed bool) error {
_, err := db.Exec("UPDATE items SET completed=? WHERE id=?", completed, id)
return err
}
func DeleteTodo(id int) error {
_, err := db.Exec("DELETE FROM items WHERE id=?", id)
return err
}
func Close() {
db.Close()
}
func toNullInt64(num int64) sql.NullInt64 {
if num == 0 {
return sql.NullInt64{Int64: 0, Valid: false}

133
db/main_db.go Normal file
View File

@ -0,0 +1,133 @@
package db
import (
"database/sql"
"encoding/hex"
"log"
"github.com/Cameron-Reed1/todo-web/auth"
"github.com/Cameron-Reed1/todo-web/types"
_ "github.com/mattn/go-sqlite3"
)
var main_db *sql.DB
func OpenMainDB(path string) {
if main_db != nil {
log.Fatal("Cannot open main DB twice!")
}
var err error
main_db, err = sql.Open("sqlite3", path)
if err != nil {
log.Fatal(err)
}
err = main_db.Ping()
if err != nil {
log.Fatal(err)
}
query := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
password_salt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
sessionId TEXT NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);`
_, err = main_db.Exec(query)
if err != nil {
log.Fatal(err)
}
}
func CloseMainDB() {
main_db.Close()
}
func CreateUser(username string, password_hash, password_salt []byte) (int64, error) {
hex_hash := hex.EncodeToString(password_hash)
hex_salt := hex.EncodeToString(password_salt)
res, err := main_db.Exec("INSERT INTO users(username, password_hash, password_salt) values(?, ?, ?)", username, hex_hash, hex_salt)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func GetUserPassHash(username string) (int64, *auth.HashSalt, error) {
hashSalt := auth.HashSalt{}
var user_id int64
var hex_hash string
var hex_salt string
row := main_db.QueryRow("SELECT id, password_hash, password_salt FROM users WHERE username=?", username)
err := row.Scan(&user_id, &hex_hash, &hex_salt)
if err != nil {
return 0, nil, err
}
hashSalt.Hash, err = hex.DecodeString(hex_hash)
if err != nil {
return 0, nil, err
}
hashSalt.Salt, err = hex.DecodeString(hex_salt)
if err != nil {
return 0, nil, err
}
return user_id, &hashSalt, nil
}
func DeleteUser(username string) error {
_, err := main_db.Exec("DELETE FROM users WHERE username=?", username)
return err
}
func AddSession(session *types.Session) error {
// fmt.Printf("New session: %s, %d\n", session.SessionId, session.UserId)
_, err := main_db.Exec("INSERT INTO sessions(sessionId, user_id) values(?, ?)", session.SessionId, session.UserId)
// fmt.Printf("Err: %v\n", err)
return err
}
func GetUserFromSession(sessionId string) (string, error) {
var username string
row := main_db.QueryRow("SELECT username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessionId=?", sessionId)
err := row.Scan(&username)
if err != nil {
return "", err
}
return username, nil
}
func GetSession(sessionId string) (*types.Session, error) {
var session types.Session
row := main_db.QueryRow("SELECT user_id FROM sessions WHERE sessionId=?", sessionId)
session.SessionId = sessionId
err := row.Scan(&session.UserId)
if err != nil {
return nil, err
}
return &session, nil
}
func DeleteSession(sessionId string) error {
_, err := main_db.Exec("DELETE FROM sessions WHERE sessionId=?", sessionId)
return err
}

233
db/user_db.go Normal file
View File

@ -0,0 +1,233 @@
package db
import (
"database/sql"
"errors"
"os"
"path"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/Cameron-Reed1/todo-web/types"
)
var userDbDir string
type UserDB struct {
DB *sql.DB
}
func SetUserDBDir(dir string) error {
os.MkdirAll(dir, 0700)
userDbDir = dir
return nil
}
func OpenUserDB(username string) (*UserDB, error) {
if strings.Contains(username, ".") || strings.Contains(username, "/") {
return nil, errors.New("Invalid username")
}
path := path.Join(userDbDir, username + ".db")
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
query := `
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start INTEGER,
due INTEGER,
text TEXT NOT NULL,
completed INTEGER NOT NULL
);`
_, err = db.Exec(query)
return &UserDB{DB: db}, err
}
func (user_db *UserDB) Close() error {
return user_db.DB.Close()
}
func (user_db *UserDB) AddTodo(todo *types.Todo) error {
res, err := user_db.DB.Exec("INSERT INTO items(start, due, text, completed) values(?, ?, ?, 0)", toNullInt64(todo.Start), toNullInt64(todo.Due), todo.Text)
if err != nil {
return err
}
todo.Id, err = res.LastInsertId()
if err != nil {
return err
}
return nil
}
func (user_db *UserDB) GetTodo(id int) (types.Todo, error) {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
row := user_db.DB.QueryRow("SELECT * FROM items WHERE id=?", id)
err := row.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
return todo, err
}
func (user_db *UserDB) GetAllTodos() ([]types.Todo, error) {
var todos []types.Todo
rows, err := user_db.DB.Query("SELECT * FROM items")
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func (user_db *UserDB) GetOverdueTodos() ([]types.Todo, error) {
var todos []types.Todo
rows, err := user_db.DB.Query("SELECT * FROM items WHERE due < ? AND due IS NOT NULL ORDER BY completed, due", time.Now().Unix())
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func (user_db *UserDB) GetTodayTodos() ([]types.Todo, error) {
var todos []types.Todo
now := time.Now()
year, month, day := now.Date()
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
rows, err := user_db.DB.Query("SELECT * FROM items WHERE (start <= ? OR start IS NULL) AND (due >= ? OR due IS NULL) ORDER BY completed, due NULLS LAST", today.Unix(), now.Unix())
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func (user_db *UserDB) GetUpcomingTodos() ([]types.Todo, error) {
var todos []types.Todo
year, month, day := time.Now().Date()
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
rows, err := user_db.DB.Query("SELECT * FROM items WHERE start > ? ORDER BY completed, start", today.Unix())
if err != nil {
return nil, err
}
for rows.Next() {
var todo types.Todo
var start sql.NullInt64
var due sql.NullInt64
err = rows.Scan(&todo.Id, &start, &due, &todo.Text, &todo.Completed)
if err != nil {
return nil, err
}
todo.Start = fromNullInt64(start)
todo.Due = fromNullInt64(due)
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func (user_db *UserDB) UpdateTodo(newValues types.Todo) error {
_, err := user_db.DB.Exec("UPDATE items SET start=?, due=?, text=? WHERE id=?", toNullInt64(newValues.Start), toNullInt64(newValues.Due), newValues.Text, newValues.Id)
return err;
}
func (user_db *UserDB) SetCompleted(id int, completed bool) error {
_, err := user_db.DB.Exec("UPDATE items SET completed=? WHERE id=?", completed, id)
return err
}
func (user_db *UserDB) DeleteTodo(id int) error {
_, err := user_db.DB.Exec("DELETE FROM items WHERE id=?", id)
return err
}

8
go.mod
View File

@ -2,6 +2,10 @@ module github.com/Cameron-Reed1/todo-web
go 1.22.6
require github.com/mattn/go-sqlite3 v1.14.22
require (
github.com/a-h/templ v0.2.747
github.com/mattn/go-sqlite3 v1.14.22
golang.org/x/crypto v0.27.0
)
require github.com/a-h/templ v0.2.747 // indirect
require golang.org/x/sys v0.25.0 // indirect

6
go.sum
View File

@ -1,4 +1,10 @@
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

21
main.go
View File

@ -12,32 +12,26 @@ import (
)
func main() {
db_path := flag.String("db", "./test.db", "Path to the sqlite3 database")
db_path := flag.String("db", "./main.db", "Path to the main sqlite3 database")
user_db_dir := flag.String("user-dbs", "./user_dbs", "Path to the directory containing per-user sqlite3 databases")
bind_port := flag.Int("p", 8080, "Port to bind to")
bind_addr := flag.String("a", "0.0.0.0", "Address to bind to")
static_dir := flag.String("static", "./static", "Path to static files")
noFront := flag.Bool("no-frontend", false, "Disable the frontend endpoints")
a := false; noBack := &a // flag.Bool("no-backend", false, "Disable the backend endpoints")
// a := false; noBack := &a // flag.Bool("no-backend", false, "Disable the backend endpoints") // This didn't really make sense
flag.Parse()
mux := http.NewServeMux()
if *noFront && *noBack {
fmt.Println("What do you want me to do?")
return
}
if !*noFront {
addFrontendEndpoints(mux, *static_dir)
}
if !*noBack {
addBackendEndpoints(mux)
}
db.Open(*db_path)
defer db.Close()
db.SetUserDBDir(*user_db_dir)
db.OpenMainDB(*db_path)
defer db.CloseMainDB()
addr := fmt.Sprintf("%s:%d", *bind_addr, *bind_port)
server := http.Server{ Addr: addr, Handler: mux }
@ -60,6 +54,9 @@ func addFrontendEndpoints(mux *http.ServeMux, static_path string) {
mux.HandleFunc("/overdue", pages.OverdueFragment)
mux.HandleFunc("/today", pages.TodayFragment)
mux.HandleFunc("/upcoming", pages.UpcomingFragment)
mux.HandleFunc("/login", pages.Login)
mux.HandleFunc("/create-account", pages.CreateAccount)
mux.HandleFunc("POST /logout", pages.Logout)
mux.HandleFunc("DELETE /delete/{id}", pages.DeleteItem)
mux.HandleFunc("PATCH /set/{id}", pages.SetItemCompleted)
mux.HandleFunc("PUT /update", pages.UpdateItem)

View File

@ -3,12 +3,18 @@ package pages
import (
"net/http"
"github.com/Cameron-Reed1/todo-web/db"
"github.com/Cameron-Reed1/todo-web/pages/templates"
)
func OverdueFragment(w http.ResponseWriter, r *http.Request) {
items, err := db.GetOverdueTodos()
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
items, err := user_db.GetOverdueTodos()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
@ -18,7 +24,14 @@ func OverdueFragment(w http.ResponseWriter, r *http.Request) {
}
func TodayFragment(w http.ResponseWriter, r *http.Request) {
items, err := db.GetTodayTodos()
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
items, err := user_db.GetTodayTodos()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
@ -28,7 +41,14 @@ func TodayFragment(w http.ResponseWriter, r *http.Request) {
}
func UpcomingFragment(w http.ResponseWriter, r *http.Request) {
items, err := db.GetUpcomingTodos()
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
items, err := user_db.GetUpcomingTodos()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return

108
pages/login.go Normal file
View File

@ -0,0 +1,108 @@
package pages
import (
"net/http"
"github.com/Cameron-Reed1/todo-web/auth"
"github.com/Cameron-Reed1/todo-web/db"
"github.com/Cameron-Reed1/todo-web/pages/templates"
)
func Login(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
if _, err := validateSession(r); err == nil {
w.Header().Add("Location", "/")
w.WriteHeader(http.StatusSeeOther)
} else {
templates.LoginPage().Render(r.Context(), w)
}
return
}
username := r.FormValue("username")
password := r.FormValue("password")
stay_logged := r.FormValue("stay-logged-in") == "on"
if username == "" || password == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
userId, hashSalt, err := db.GetUserPassHash(username)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if auth.Validate(hashSalt.Hash, hashSalt.Salt, []byte(password)) {
session, err := createSession(userId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Add("Set-Cookie", session.ToCookie(stay_logged))
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
func CreateAccount(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
if _, err := validateSession(r); err == nil {
w.Header().Add("Location", "/")
w.WriteHeader(http.StatusSeeOther)
} else {
templates.CreateAccountBox().Render(r.Context(), w)
}
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO: validate credentials
// Ensure the username is valid and is not taken
// Ensure that the password meets requirements
hashSalt, err := auth.Hash([]byte(password), nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
user_id, err := db.CreateUser(username, hashSalt.Hash, hashSalt.Salt)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
session, err := createSession(user_id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Add("Set-Cookie", session.ToCookie(false))
}
func Logout(w http.ResponseWriter, r *http.Request) {
session, err := validateSession(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = db.DeleteSession(session)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Add("Set-Cookie", "session=;expires=Thu, 01 Jan 1970 00:00:00 UTC;samesite=strict;secure;HTTPonly")
w.WriteHeader(http.StatusOK)
}

View File

@ -7,5 +7,12 @@ import (
)
func RootPage(w http.ResponseWriter, r *http.Request) {
templates.RootPage().Render(r.Context(), w)
username, err := validateSessionAndGetUsername(r)
if err != nil {
w.Header().Add("Location", "/login")
w.WriteHeader(http.StatusFound)
return
}
templates.RootPage(username).Render(r.Context(), w)
}

View File

@ -0,0 +1,70 @@
package templates
templ loginSkeleton() {
<!Doctype HTML>
<html lang="en-US">
<head>
<title>Todo login</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/css/login.css"/>
<script src="/js/login.js"></script>
<script src="/js/lib/htmx.min.js"></script>
</head>
<body>
{ children... }
</body>
</html>
}
templ LoginPage() {
@loginSkeleton() {
<form id="login-box" hx-post="/login" hx-swap="none">
<h1>Login</h1>
<div>
<label for="username">Username</label><br/>
<input id="username" name="username" type="text" required/>
<div style="margin: 20px"></div>
<label for="password">Password</label><br/>
<input id="password" name="password" type="password" required/>
<label for="stay-logged-in">Keep me logged in</label>
<input id="stay-logged-in" name="stay-logged-in" type="checkbox"/><br/>
<button type="submit">Log in</button>
<a href="/create-account">Create Account</a>
</div>
</form>
}
}
templ CreateAccountBox() {
@loginSkeleton() {
<form id="login-box" hx-post="/create-account">
<h1>Create Account</h1>
<div>
<label for="username">Username</label><br/>
<input id="username" name="username" type="text" required/>
<div style="margin: 20px"></div>
<label for="password">Password</label><br/>
<input id="password" name="password" type="password" required/>
<div style="margin: 20px"></div>
<label for="confirm-password">Confirm Password</label><br/>
<input id="confirm-password" name="confirm-password" type="password" required/>
<button type="submit">Create account</button>
</div>
</form>
}
}

View File

@ -0,0 +1,131 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.747
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func loginSkeleton() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype HTML><html lang=\"en-US\"><head><title>Todo login</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link rel=\"stylesheet\" href=\"/css/login.css\"><script src=\"/js/login.js\"></script><script src=\"/js/lib/htmx.min.js\"></script></head><body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func LoginPage() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form id=\"login-box\" hx-post=\"/login\" hx-swap=\"none\"><h1>Login</h1><div><label for=\"username\">Username</label><br><input id=\"username\" name=\"username\" type=\"text\" required><div style=\"margin: 20px\"></div><label for=\"password\">Password</label><br><input id=\"password\" name=\"password\" type=\"password\" required> <label for=\"stay-logged-in\">Keep me logged in</label> <input id=\"stay-logged-in\" name=\"stay-logged-in\" type=\"checkbox\"><br><button type=\"submit\">Log in</button> <a href=\"/create-account\">Create Account</a></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = loginSkeleton().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func CreateAccountBox() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form id=\"login-box\" hx-post=\"/create-account\"><h1>Create Account</h1><div><label for=\"username\">Username</label><br><input id=\"username\" name=\"username\" type=\"text\" required><div style=\"margin: 20px\"></div><label for=\"password\">Password</label><br><input id=\"password\" name=\"password\" type=\"password\" required><div style=\"margin: 20px\"></div><label for=\"confirm-password\">Confirm Password</label><br><input id=\"confirm-password\" name=\"confirm-password\" type=\"password\" required> <button type=\"submit\">Create account</button></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = loginSkeleton().Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}

View File

@ -6,7 +6,7 @@ import (
"github.com/Cameron-Reed1/todo-web/types"
)
templ RootPage() {
templ RootPage(username string) {
<!Doctype HTML>
<html lang="en-US" data-show-completed="false">
<head>
@ -26,14 +26,26 @@ templ RootPage() {
<body>
<nav>
<div id="nav-container">
<div id="nav-left">
<a id="new-button" href="#create-item">New</a>
<div id="nav-left" class="nav-section">
<a id="new-button" class="focus-highlight" href="#create-item">New</a>
</div>
<div id="nav-center"></div>
<div id="nav-right">
<div id="nav-center" class="nav-section"></div>
<div id="nav-right" class="nav-section">
<div>
<label for="show-completed">Show completed</label>
<input id="show-completed" type="checkbox" name="show-completed"/>
</div>
<div id="profile">
<div id="profile-icon" class="focus-highlight">
<i class="fa-solid fa-user"></i>
<i class="fa-solid fa-caret-down"></i>
</div>
<div id="profile-dropdown">
<div id="profile-name" class="focus-highlight">{ username }</div>
<div id="profile-logout" class="focus-highlight" hx-post="/logout" hx-swap="none">Log out</div>
</div>
</div>
</div>
</div>
</nav>

View File

@ -14,7 +14,7 @@ import (
"github.com/Cameron-Reed1/todo-web/types"
)
func RootPage() templ.Component {
func RootPage(username string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
@ -32,7 +32,20 @@ func RootPage() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype HTML><html lang=\"en-US\" data-show-completed=\"false\"><head><title>Todo</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link rel=\"stylesheet\" href=\"/css/styles.css\"><script src=\"/js/script.js\"></script><script src=\"/js/lib/htmx.min.js\"></script><!-- Font Awesome --><script src=\"https://kit.fontawesome.com/469cdddb31.js\" crossorigin=\"anonymous\"></script></head><body><nav><div id=\"nav-container\"><div id=\"nav-left\"><a id=\"new-button\" href=\"#create-item\">New</a></div><div id=\"nav-center\"></div><div id=\"nav-right\"><label for=\"show-completed\">Show completed</label> <input id=\"show-completed\" type=\"checkbox\" name=\"show-completed\"></div></div></nav><div id=\"main-content\"><div id=\"lists\"><div id=\"overdue-list\" class=\"todo-list\"><div class=\"todo-list-title\">Overdue</div><div class=\"todo-list-items\" hx-get=\"/overdue\" hx-trigger=\"load\" hx-swap=\"outerHTML\"></div><div class=\"new-item\"></div></div><div id=\"today-list\" class=\"todo-list\"><div class=\"todo-list-title\">Today</div><div class=\"todo-list-items\" hx-get=\"/today\" hx-trigger=\"load\" hx-swap=\"outerHTML\"></div><div class=\"new-item\"></div></div><div id=\"upcoming-list\" class=\"todo-list\"><div class=\"todo-list-title\">Upcoming</div><div class=\"todo-list-items\" hx-get=\"/upcoming\" hx-trigger=\"load\" hx-swap=\"outerHTML\"></div><div class=\"new-item\"></div></div></div><form id=\"create-item\" hx-post=\"/new\" hx-swap=\"none\"><div class=\"form-title\">Create new Todo</div><div class=\"form-container\"><div class=\"form-column\"><label for=\"name\">Name</label><br><input id=\"create-item-name\" type=\"text\" name=\"name\"><br><label for=\"start\">Start</label><br><input id=\"create-item-start\" name=\"start\" type=\"datetime-local\"><br><label for=\"due\">Due</label><br><input id=\"create-item-due\" name=\"due\" type=\"datetime-local\"></div><div class=\"form-column\"></div></div><div class=\"form-button-container\"><button id=\"create-save\" class=\"form-save-button button\" type=\"submit\">Save</button> <a class=\"form-close-button button\" href=\"#\">Close</a></div></form><form id=\"edit-item\" data-id=\"\" hx-put=\"/update\" hx-swap=\"outerHTML\"><div class=\"form-title\">Edit Todo</div><div class=\"form-container\"><div class=\"form-column\"><label for=\"name\">Name</label><br><input id=\"edit-item-name\" type=\"text\" name=\"name\"><br><label for=\"start\">Start</label><br><input id=\"edit-item-start\" name=\"start\" type=\"datetime-local\"><br><label for=\"due\">Due</label><br><input id=\"edit-item-due\" name=\"due\" type=\"datetime-local\"></div><div class=\"form-column\"></div></div><div class=\"form-button-container\"><button id=\"edit-save\" class=\"form-save-button button\" type=\"submit\">Save</button> <a class=\"form-close-button button\" href=\"#\">Close</a></div></form></div></body></html>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype HTML><html lang=\"en-US\" data-show-completed=\"false\"><head><title>Todo</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link rel=\"stylesheet\" href=\"/css/styles.css\"><script src=\"/js/script.js\"></script><script src=\"/js/lib/htmx.min.js\"></script><!-- Font Awesome --><script src=\"https://kit.fontawesome.com/469cdddb31.js\" crossorigin=\"anonymous\"></script></head><body><nav><div id=\"nav-container\"><div id=\"nav-left\" class=\"nav-section\"><a id=\"new-button\" class=\"focus-highlight\" href=\"#create-item\">New</a></div><div id=\"nav-center\" class=\"nav-section\"></div><div id=\"nav-right\" class=\"nav-section\"><div><label for=\"show-completed\">Show completed</label> <input id=\"show-completed\" type=\"checkbox\" name=\"show-completed\"></div><div id=\"profile\"><div id=\"profile-icon\" class=\"focus-highlight\"><i class=\"fa-solid fa-user\"></i> <i class=\"fa-solid fa-caret-down\"></i></div><div id=\"profile-dropdown\"><div id=\"profile-name\" class=\"focus-highlight\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 44, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div id=\"profile-logout\" class=\"focus-highlight\" hx-post=\"/logout\" hx-swap=\"none\">Log out</div></div></div></div></div></nav><div id=\"main-content\"><div id=\"lists\"><div id=\"overdue-list\" class=\"todo-list\"><div class=\"todo-list-title\">Overdue</div><div class=\"todo-list-items\" hx-get=\"/overdue\" hx-trigger=\"load\" hx-swap=\"outerHTML\"></div><div class=\"new-item\"></div></div><div id=\"today-list\" class=\"todo-list\"><div class=\"todo-list-title\">Today</div><div class=\"todo-list-items\" hx-get=\"/today\" hx-trigger=\"load\" hx-swap=\"outerHTML\"></div><div class=\"new-item\"></div></div><div id=\"upcoming-list\" class=\"todo-list\"><div class=\"todo-list-title\">Upcoming</div><div class=\"todo-list-items\" hx-get=\"/upcoming\" hx-trigger=\"load\" hx-swap=\"outerHTML\"></div><div class=\"new-item\"></div></div></div><form id=\"create-item\" hx-post=\"/new\" hx-swap=\"none\"><div class=\"form-title\">Create new Todo</div><div class=\"form-container\"><div class=\"form-column\"><label for=\"name\">Name</label><br><input id=\"create-item-name\" type=\"text\" name=\"name\"><br><label for=\"start\">Start</label><br><input id=\"create-item-start\" name=\"start\" type=\"datetime-local\"><br><label for=\"due\">Due</label><br><input id=\"create-item-due\" name=\"due\" type=\"datetime-local\"></div><div class=\"form-column\"></div></div><div class=\"form-button-container\"><button id=\"create-save\" class=\"form-save-button button\" type=\"submit\">Save</button> <a class=\"form-close-button button\" href=\"#\">Close</a></div></form><form id=\"edit-item\" data-id=\"\" hx-put=\"/update\" hx-swap=\"outerHTML\"><div class=\"form-title\">Edit Todo</div><div class=\"form-container\"><div class=\"form-column\"><label for=\"name\">Name</label><br><input id=\"edit-item-name\" type=\"text\" name=\"name\"><br><label for=\"start\">Start</label><br><input id=\"edit-item-start\" name=\"start\" type=\"datetime-local\"><br><label for=\"due\">Due</label><br><input id=\"edit-item-due\" name=\"due\" type=\"datetime-local\"></div><div class=\"form-column\"></div></div><div class=\"form-button-container\"><button id=\"edit-save\" class=\"form-save-button button\" type=\"submit\">Save</button> <a class=\"form-close-button button\" href=\"#\">Close</a></div></form></div></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -63,21 +76,21 @@ func TodoItem(item types.Todo) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("item-%d", item.Id))
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("item-%d", item.Id))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 116, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 128, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -85,12 +98,12 @@ func TodoItem(item types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Id))
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Id))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 116, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 128, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -98,12 +111,12 @@ func TodoItem(item types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Start))
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Start))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 116, Col: 150}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 128, Col: 150}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -111,12 +124,12 @@ func TodoItem(item types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Due))
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Due))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 116, Col: 191}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 128, Col: 191}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -134,12 +147,12 @@ func TodoItem(item types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/set/%d", item.Id))))
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/set/%d", item.Id))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 117, Col: 137}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 129, Col: 137}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -147,12 +160,12 @@ func TodoItem(item types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Text)
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 118, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 130, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -160,12 +173,12 @@ func TodoItem(item types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/delete/%d", item.Id))
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/delete/%d", item.Id))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 121, Col: 101}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 133, Col: 101}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -190,21 +203,21 @@ func OobTodoItem(targetSelector string, item types.Todo) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div hx-swap-oob=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s:%s", "afterend", targetSelector))
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s:%s", "afterend", targetSelector))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 127, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 139, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -237,21 +250,21 @@ func TodoList(fillerText string, items []types.Todo) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var12 = templ.NopComponent
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"todo-list-items\" data-item-count=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(items)))
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(items)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 133, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 145, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -259,12 +272,12 @@ func TodoList(fillerText string, items []types.Todo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fillerText)
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fillerText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 134, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/templates/root.templ`, Line: 146, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

63
pages/utils.go Normal file
View File

@ -0,0 +1,63 @@
package pages
import (
"net/http"
"github.com/Cameron-Reed1/todo-web/auth"
"github.com/Cameron-Reed1/todo-web/db"
"github.com/Cameron-Reed1/todo-web/types"
)
func createSession(user_id int64) (*types.Session, error) {
session, err := auth.CreateSessionFor(user_id)
if err != nil {
return nil, err
}
err = db.AddSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func validateSession(r *http.Request) (string, error) {
cookie, err := r.Cookie("session")
if err != nil {
return "", err
}
_, err = db.GetUserFromSession(cookie.Value)
// session, err := db.GetSession(cookie.Value)
return cookie.Value, err
}
func validateSessionAndGetUsername(r *http.Request) (string, error) {
cookie, err := r.Cookie("session")
if err != nil {
return "", err
}
return db.GetUserFromSession(cookie.Value)
}
func validateSessionAndGetUserDB(r *http.Request) (*db.UserDB, error) {
cookie, err := r.Cookie("session")
if err != nil {
return nil, err
}
username, err := db.GetUserFromSession(cookie.Value)
// session, err := db.GetSession(cookie.Value)
if err != nil {
return nil, err
}
user_db, err := db.OpenUserDB(username)
if err != nil {
return nil, err
}
return user_db, nil
}

View File

@ -6,20 +6,25 @@ import (
"strconv"
"time"
"github.com/Cameron-Reed1/todo-web/db"
"github.com/Cameron-Reed1/todo-web/pages/templates"
"github.com/Cameron-Reed1/todo-web/types"
)
func CreateItem(w http.ResponseWriter, r *http.Request) {
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
var todo types.Todo
var err error
todo.Text = r.FormValue("name")
start := r.FormValue("start")
due := r.FormValue("due")
fmt.Printf("Create item request: %s: %s - %s\n", todo.Text, start, due)
// fmt.Printf("Create item request: %s: %s - %s\n", todo.Text, start, due)
if start != "" {
todo.Start, err = strconv.ParseInt(start, 10, 64)
@ -43,9 +48,9 @@ func CreateItem(w http.ResponseWriter, r *http.Request) {
todo.Due = 0
}
fmt.Printf("New item: %s: %d - %d\n", todo.Text, todo.Start, todo.Due)
// fmt.Printf("New item: %s: %d - %d\n", todo.Text, todo.Start, todo.Due)
err = db.AddTodo(&todo)
err = user_db.AddTodo(&todo)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
@ -64,6 +69,13 @@ func CreateItem(w http.ResponseWriter, r *http.Request) {
}
func DeleteItem(w http.ResponseWriter, r *http.Request) {
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
@ -72,7 +84,7 @@ func DeleteItem(w http.ResponseWriter, r *http.Request) {
return
}
err = db.DeleteTodo(id)
err = user_db.DeleteTodo(id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
@ -83,6 +95,13 @@ func DeleteItem(w http.ResponseWriter, r *http.Request) {
}
func SetItemCompleted(w http.ResponseWriter, r *http.Request) {
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
@ -92,7 +111,7 @@ func SetItemCompleted(w http.ResponseWriter, r *http.Request) {
}
completed := r.FormValue("completed") == "on"
if err = db.SetCompleted(id, completed); err != nil {
if err = user_db.SetCompleted(id, completed); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
@ -101,8 +120,14 @@ func SetItemCompleted(w http.ResponseWriter, r *http.Request) {
}
func UpdateItem(w http.ResponseWriter, r *http.Request) {
user_db, err := validateSessionAndGetUserDB(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
defer user_db.Close()
var todo types.Todo
var err error
idStr := r.FormValue("id")
todo.Text = r.FormValue("name")
@ -138,9 +163,9 @@ func UpdateItem(w http.ResponseWriter, r *http.Request) {
todo.Due = 0
}
fmt.Printf("New values:\n(%d) %s: %d - %d\n\n", todo.Id, todo.Text, todo.Start, todo.Due)
// fmt.Printf("New values:\n(%d) %s: %d - %d\n\n", todo.Id, todo.Text, todo.Start, todo.Due)
err = db.UpdateTodo(todo)
err = user_db.UpdateTodo(todo)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return

56
static/css/login.css Normal file
View File

@ -0,0 +1,56 @@
body {
margin: 0;
padding: 0;
height: 100vh;
height: 100lvh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: sans-serif;
}
#login-box {
padding: 0 10px;
width: 275px;
height: min(calc(100lvh - 110px), 450px);
border: 4px solid black;
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
label {
margin-left: 4px;
}
input[type="text"],
input[type="password"] {
width: 267px;
}
button {
padding: 10px 20px;
text-decoration: none;
border: 2px solid;
border-radius: 6px;
box-shadow: black 2px 2px 3px 0;
background-color: white;
font-size: 1rem;
cursor: pointer;
margin: 35px 0 25px 0;
color: blue;
}
a {
display: block;
font-size: .85rem;
text-decoration: none;
}
h1 {
text-align: center;
}

View File

@ -5,6 +5,7 @@
nav {
height: 48px;
line-height: 48px;
border-bottom: 2px solid black;
}
@ -16,12 +17,17 @@ nav {
align-items: center;
}
.nav-section {
padding: 0 13px;
display: flex;
flex-direction: row;
gap: 15px;
}
#new-button {
width: 100px;
text-transform: uppercase;
cursor: pointer;
text-align: center;
line-height: 48px;
font-size: 2em;
font-weight: 600;
font-family: sans-serif;
@ -30,10 +36,44 @@ nav {
display: inline-block;
}
#new-button:hover {
.focus-highlight:hover {
background-color: #ddd;
}
#profile {
position: relative;
}
#profile-icon {
padding: 0 10px;
cursor: pointer;
}
#profile-dropdown {
position: absolute;
top: 48px;
right: 0px;
height: 0;
width: 100px;
z-index: 9999;
border: 0px solid black;
transition: height .5s, border-width 0s .5s;
overflow: hidden;
background-color: white;
}
#profile:hover > #profile-dropdown {
height: 64px;
border-width: 2px;
transition: height .5s, border-width 0s;
}
#profile-dropdown > div {
cursor: pointer;
line-height: 32px;
padding: 0 4px;
}
#main-content {
height: calc(100vh - 100px);
height: calc(100lvh - 100px);

16
static/js/login.js Normal file
View File

@ -0,0 +1,16 @@
function on_load() {
const login_box = document.getElementById("login-box");
login_box.addEventListener("htmx:afterRequest", function(evt) {
if (evt.detail.successful) {
window.location.pathname = "/";
}
});
}
if (document.readyState === "completed") {
on_load();
} else {
document.addEventListener("DOMContentLoaded", on_load);
}

View File

@ -1,4 +1,12 @@
function on_load() {
let logout_button = document.getElementById("profile-logout");
logout_button.addEventListener("htmx:afterRequest", function(evt) {
if (evt.detail.successful) {
window.location.reload();
}
});
let create_start_input = document.getElementById("create-item-start");
let create_due_input = document.getElementById("create-item-due");

17
types/session.go Normal file
View File

@ -0,0 +1,17 @@
package types
import "fmt"
type Session struct {
UserId int64
SessionId string
}
func (session *Session) ToCookie(stay_logged bool) string {
age := ""
if stay_logged {
age = "max-age=31536000;"
}
return fmt.Sprintf("session=%s;%ssamesite=strict;secure;HTTPonly", session.SessionId, age)
}