From a55cfe8205d5cb2fb0948c173f23ad71d6614d13 Mon Sep 17 00:00:00 2001 From: Steven Le Rouzic Date: Sat, 27 Apr 2024 23:55:31 +0200 Subject: Rename everything to locker/lock --- build.bat | 4 + css/input.css | 3 + database.go | 6 +- go.mod | 2 +- lock.db | Bin 0 -> 28672 bytes locker.go | 323 +++++++++++++++++++++++++++++++++++++++++++++++++ model/lock.go | 110 +++++++++++++++++ model/timer.go | 110 ----------------- session.go | 6 +- static/style.css | 10 +- tailwind.config.js | 8 ++ timer.db | Bin 28672 -> 0 bytes timer.go | 323 ------------------------------------------------- view/lock.templ | 80 ++++++++++++ view/locks_list.templ | 51 ++++++++ view/login.templ | 2 +- view/main.templ | 4 +- view/timer.templ | 80 ------------ view/timers_list.templ | 51 -------- 19 files changed, 590 insertions(+), 583 deletions(-) create mode 100644 build.bat create mode 100644 css/input.css create mode 100644 lock.db create mode 100644 locker.go create mode 100644 model/lock.go delete mode 100644 model/timer.go create mode 100644 tailwind.config.js delete mode 100644 timer.db delete mode 100644 timer.go create mode 100644 view/lock.templ create mode 100644 view/locks_list.templ delete mode 100644 view/timer.templ delete mode 100644 view/timers_list.templ diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..5f7da18 --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +templ generate +tailwindcss -i css\input.css -o static\style.css --minify + + diff --git a/css/input.css b/css/input.css new file mode 100644 index 0000000..04b35af --- /dev/null +++ b/css/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/database.go b/database.go index c53b828..425f98a 100644 --- a/database.go +++ b/database.go @@ -6,7 +6,7 @@ import ( "golang.org/x/crypto/bcrypt" - "stevenlr.com/timer/model" + "stevenlr.com/locker/model" ) func initializeDatabaseV1(db *sql.DB) error { @@ -22,7 +22,7 @@ func initializeDatabaseV1(db *sql.DB) error { } _, err = tx.Exec(` - CREATE TABLE Timer ( + CREATE TABLE Lock ( Id BLOB NOT NULL UNIQUE, Name TEXT NOT NULL, StartTime TEXT NOT NULL, @@ -74,7 +74,7 @@ func migrateDatabaseV2(db *sql.DB) error { return err } - _, err = tx.Exec("CREATE INDEX TimerTokenIndex ON Timer(Token)") + _, err = tx.Exec("CREATE INDEX LockTokenIndex ON Lock(Token)") if err != nil { return err } diff --git a/go.mod b/go.mod index ec76474..7723c83 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module stevenlr.com/timer +module stevenlr.com/locker go 1.22.2 diff --git a/lock.db b/lock.db new file mode 100644 index 0000000..a5c6824 Binary files /dev/null and b/lock.db differ diff --git a/locker.go b/locker.go new file mode 100644 index 0000000..7bc6cc4 --- /dev/null +++ b/locker.go @@ -0,0 +1,323 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "net/http" + "strings" + + "golang.org/x/crypto/bcrypt" + + _ "github.com/mattn/go-sqlite3" + + "stevenlr.com/locker/model" + "stevenlr.com/locker/utils" + "stevenlr.com/locker/view" +) + +type LockServer struct { + db *sql.DB + sessions Sessions +} + +func (server *LockServer) findCurrentUser(w http.ResponseWriter, r *http.Request) *model.User { + return server.sessions.FindCurrentUser(server.db, w, r) +} + +func (server *LockServer) handleNotFound(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + view.Error404().Render(context.Background(), w) +} + +func (server *LockServer) handleMain(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) + if r.URL.Path == "/" { + locks := make([]model.Lock, 0) + if currentUser != nil { + locks = model.GetLocksForUser(server.db, currentUser.Id) + } + view.Main(view.LocksList(locks, currentUser != nil), currentUser).Render(context.Background(), w) + } else { + server.handleNotFound(w, r) + } +} + +func (server *LockServer) handleLock(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) + if currentUser == nil { + server.handleNotFound(w, r) + return + } + + var id model.UUID + if err := id.Scan(r.PathValue("lockId")); err != nil { + server.handleNotFound(w, r) + return + } + + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock != nil && lock.Owner == currentUser.Id { + view.Main(view.LockView(*lock), currentUser).Render(context.Background(), w) + } else { + server.handleNotFound(w, r) + } +} + +func (server *LockServer) handleLockAddTimeCommon(w http.ResponseWriter, r *http.Request, lock *model.Lock) bool { + if lock.IsFinished() { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Lock already finished")) + return false + } + + duration, err := utils.ParseDuration(r.FormValue("timeToAdd")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return false + } + + lock.EndTime.Add(duration) + res := model.UpdateLockEndTime(server.db, lock.Id, lock.EndTime, lock.Owner) + if !res { + w.WriteHeader(http.StatusInternalServerError) + return false + } + + return true +} + +func (server *LockServer) handleLockAddTime(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) + if currentUser == nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + var id model.UUID + if err := id.Scan(r.PathValue("lockId")); err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + if !server.handleLockAddTimeCommon(w, r, lock) { + return + } + + view.LockInfo(*lock).Render(context.Background(), w) +} + +func (server *LockServer) handleApiLockAddTime(w http.ResponseWriter, r *http.Request) { + var id model.UUID + if err := id.Scan(r.PathValue("lockId")); err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + lock := model.GetLockWithToken(server.db, id, r.FormValue("token")) + if lock == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + if !server.handleLockAddTimeCommon(w, r, lock) { + return + } +} + +func (server *LockServer) handleGetLockToken(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) + if currentUser == nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + var id model.UUID + if err := id.Scan(r.PathValue("lockId")); err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock == nil { + server.handleNotFound(w, r) + return + } + + w.Write([]byte(fmt.Sprint("", lock.Token, ""))) +} + +func (server *LockServer) handleResetLockToken(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) + if currentUser == nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + var id model.UUID + if err := id.Scan(r.PathValue("lockId")); err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + res := model.RegenerateLockToken(server.db, lock.Id, currentUser.Id) + if !res { + w.WriteHeader(http.StatusInternalServerError) + return + } + + view.LockTokenForm(*lock).Render(context.Background(), w) +} + +func (server *LockServer) handleDeleteLock(w http.ResponseWriter, r *http.Request) { + user := server.findCurrentUser(w, r) + if user == nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + var id model.UUID + if err := id.Scan(r.PathValue("lockId")); err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + success := model.DeleteLock(server.db, id, user.Id) + if !success { + w.WriteHeader(http.StatusNotFound) + } +} + +func (server *LockServer) handleCreateLock(w http.ResponseWriter, r *http.Request) { + lockName := strings.TrimSpace(r.FormValue("lockName")) + + user := server.findCurrentUser(w, r) + if user == nil { + w.WriteHeader(http.StatusBadRequest) + view.LockCreateForm(lockName, "You are not signed in").Render(context.Background(), w) + return + } + + days, err := utils.ParseNumber(r.FormValue("days")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + view.LockCreateForm(lockName, "Error parsing days").Render(context.Background(), w) + return + } + + hours, err := utils.ParseNumber(r.FormValue("hours")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + view.LockCreateForm(lockName, "Error parsing hours").Render(context.Background(), w) + return + } + + tx, err := server.db.Begin() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + view.LockCreateForm(lockName, "Internal server error").Render(context.Background(), w) + return + } + defer tx.Rollback() + + if lockName == "" { + w.WriteHeader(http.StatusBadRequest) + view.LockCreateForm("", "Lock name cannot be empty").Render(context.Background(), w) + return + } + + err = model.InsertLock(tx, lockName, int(((max(days, 0)*24)+max(hours, 0))*3600), user.Id) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + view.LockCreateForm(lockName, "Internal server error").Render(context.Background(), w) + return + } + + tx.Commit() + + locks := model.GetLocksForUser(server.db, user.Id) + view.LocksList(locks, user != nil).Render(context.Background(), w) +} + +func (server *LockServer) handlePostLogin(w http.ResponseWriter, r *http.Request) { + if server.findCurrentUser(w, r) != nil { + utils.HtmxRedirect(w, "/") + return + } + + userName := r.FormValue("user") + userPass := r.FormValue("password") + + user := model.GetUserByName(server.db, userName) + if user == nil { + w.WriteHeader(http.StatusBadRequest) + view.LoginFormError(nil, "Incorrect credentials").Render(context.Background(), w) + return + } + + err := bcrypt.CompareHashAndPassword(user.Password, []byte(userPass)) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + view.LoginFormError(nil, "Incorrect credentials").Render(context.Background(), w) + return + } + + if err := server.sessions.StartSession(user.Id, w); err == nil { + utils.HtmxRedirect(w, "/") + } else { + w.WriteHeader(http.StatusInternalServerError) + view.LoginFormError(nil, "Internal server error").Render(context.Background(), w) + } +} + +func (server *LockServer) handlePostLogout(w http.ResponseWriter, r *http.Request) { + server.sessions.EndSession(w, r) + utils.HtmxRedirect(w, "/") +} + +func main() { + log.Println("Starting...") + + db, err := sql.Open("sqlite3", "file:lock.db") + if err != nil { + log.Fatalln(err) + } + defer db.Close() + + if err := InitializeDatabase(db); err != nil { + log.Fatalln(err) + } + + myServer := LockServer{db: db, sessions: MakeSessions()} + + fs := http.FileServer(http.Dir("static/")) + http.Handle("GET /static/", http.StripPrefix("/static/", fs)) + + http.HandleFunc("POST /login", myServer.handlePostLogin) + http.HandleFunc("POST /logout", myServer.handlePostLogout) + http.HandleFunc("GET /lock/{lockId}", myServer.handleLock) + http.HandleFunc("POST /lock/{lockId}/addTime", myServer.handleLockAddTime) + http.HandleFunc("POST /api/lock/{lockId}/addTime", myServer.handleApiLockAddTime) + http.HandleFunc("DELETE /lock/{lockId}", myServer.handleDeleteLock) + http.HandleFunc("POST /lock/{lockId}/resetToken", myServer.handleResetLockToken) + http.HandleFunc("GET /lock/{lockId}/token", myServer.handleGetLockToken) + http.HandleFunc("PUT /lock", myServer.handleCreateLock) + http.HandleFunc("GET /", myServer.handleMain) + + log.Println("Started!") + http.ListenAndServe("0.0.0.0:80", nil) +} diff --git a/model/lock.go b/model/lock.go new file mode 100644 index 0000000..555d31e --- /dev/null +++ b/model/lock.go @@ -0,0 +1,110 @@ +package model + +import ( + "database/sql" + "log" + "time" + + "stevenlr.com/locker/utils" +) + +func GenerateLockToken() (string, error) { + return utils.GenerateRandomString(66) +} + +type Lock struct { + Id UUID + Name string + StartTime Time + EndTime Time + Owner UUID + Token string +} + +func (self Lock) IsFinished() bool { + return MakeTimeNow().Compare(self.EndTime) >= 0 +} + +func InsertLock(tx *sql.Tx, name string, seconds int, ownerId UUID) error { + now := MakeTimeNow() + end := Time(time.Time(now).Add(time.Duration(seconds) * time.Second)) + id := MakeUUID() + token, _ := GenerateLockToken() + _, err := tx.Exec(` + INSERT INTO Lock VALUES ($1, $2, $3, $4, $5, $6)`, id, name, now, end, ownerId, token) + return err +} + +func GetLocksForUser(db *sql.DB, owner UUID) []Lock { + rows, err := db.Query("SELECT Id, Name FROM Lock WHERE Owner=$1", owner) + if err != nil { + log.Fatalln(err) + } + + locks := []Lock{} + for rows.Next() { + var t Lock + if err := rows.Scan(&t.Id, &t.Name); err == nil { + locks = append(locks, t) + } + } + + return locks +} + +func GetLockForUser(db *sql.DB, id UUID, userId UUID) *Lock { + row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Lock WHERE Id=$1 AND Owner=$2", id, userId) + + var t Lock + if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { + return &t + } + + return nil +} + +func GetLockWithToken(db *sql.DB, id UUID, token string) *Lock { + row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Lock WHERE Id=$1 AND Token=$2", id, token) + + var t Lock + if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { + return &t + } + + return nil +} + +func DeleteLock(db *sql.DB, id UUID, userId UUID) bool { + res, err := db.Exec("DELETE FROM Lock WHERE Id=$1 AND Owner=$2", id, userId) + if err != nil { + return false + } + + affected, err := res.RowsAffected() + return err == nil && affected == 1 +} + +func UpdateLockEndTime(db *sql.DB, id UUID, endTime Time, userId UUID) bool { + res, err := db.Exec("UPDATE Lock SET EndTime=$1 WHERE Id=$2 AND Owner=$3", endTime, id, userId) + if err != nil { + return false + } + + affected, err := res.RowsAffected() + return err == nil && affected == 1 +} + +func RegenerateLockToken(db *sql.DB, id UUID, userId UUID) bool { + newToken, err := GenerateLockToken() + if err != nil { + return false + } + + res, err := db.Exec("UPDATE Lock SET Token=$1 WHERE Id=$2 AND Owner=$3", newToken, id, userId) + if err != nil { + return false + } + + affected, err := res.RowsAffected() + return err == nil && affected == 1 +} diff --git a/model/timer.go b/model/timer.go deleted file mode 100644 index 3f13d0d..0000000 --- a/model/timer.go +++ /dev/null @@ -1,110 +0,0 @@ -package model - -import ( - "database/sql" - "log" - "time" - - "stevenlr.com/timer/utils" -) - -func GenerateTimerToken() (string, error) { - return utils.GenerateRandomString(66) -} - -type Timer struct { - Id UUID - Name string - StartTime Time - EndTime Time - Owner UUID - Token string -} - -func (self Timer) IsFinished() bool { - return MakeTimeNow().Compare(self.EndTime) >= 0 -} - -func InsertTimer(tx *sql.Tx, name string, seconds int, ownerId UUID) error { - now := MakeTimeNow() - end := Time(time.Time(now).Add(time.Duration(seconds) * time.Second)) - id := MakeUUID() - token, _ := GenerateTimerToken() - _, err := tx.Exec(` - INSERT INTO Timer VALUES ($1, $2, $3, $4, $5, $6)`, id, name, now, end, ownerId, token) - return err -} - -func GetTimersForUser(db *sql.DB, owner UUID) []Timer { - rows, err := db.Query("SELECT Id, Name FROM Timer WHERE Owner=$1", owner) - if err != nil { - log.Fatalln(err) - } - - timers := []Timer{} - for rows.Next() { - var t Timer - if err := rows.Scan(&t.Id, &t.Name); err == nil { - timers = append(timers, t) - } - } - - return timers -} - -func GetTimerForUser(db *sql.DB, id UUID, userId UUID) *Timer { - row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Timer WHERE Id=$1 AND Owner=$2", id, userId) - - var t Timer - if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { - return &t - } - - return nil -} - -func GetTimerWithToken(db *sql.DB, id UUID, token string) *Timer { - row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Timer WHERE Id=$1 AND Token=$2", id, token) - - var t Timer - if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { - return &t - } - - return nil -} - -func DeleteTimer(db *sql.DB, id UUID, userId UUID) bool { - res, err := db.Exec("DELETE FROM Timer WHERE Id=$1 AND Owner=$2", id, userId) - if err != nil { - return false - } - - affected, err := res.RowsAffected() - return err == nil && affected == 1 -} - -func UpdateTimerEndTime(db *sql.DB, id UUID, endTime Time, userId UUID) bool { - res, err := db.Exec("UPDATE Timer SET EndTime=$1 WHERE Id=$2 AND Owner=$3", endTime, id, userId) - if err != nil { - return false - } - - affected, err := res.RowsAffected() - return err == nil && affected == 1 -} - -func RegenerateTimerToken(db *sql.DB, id UUID, userId UUID) bool { - newToken, err := GenerateTimerToken() - if err != nil { - return false - } - - res, err := db.Exec("UPDATE Timer SET Token=$1 WHERE Id=$2 AND Owner=$3", newToken, id, userId) - if err != nil { - return false - } - - affected, err := res.RowsAffected() - return err == nil && affected == 1 -} diff --git a/session.go b/session.go index e32041f..9d86bc7 100644 --- a/session.go +++ b/session.go @@ -5,8 +5,8 @@ import ( "errors" "net/http" - "stevenlr.com/timer/model" - "stevenlr.com/timer/utils" + "stevenlr.com/locker/model" + "stevenlr.com/locker/utils" ) func generateSessionId() (string, error) { @@ -21,7 +21,7 @@ type Session struct { UserId model.UUID } -const sessionCookieName = "timerSession" +const sessionCookieName = "LockerSession" func removeCookie(cookieName string, w http.ResponseWriter) { cookie := http.Cookie{ diff --git a/static/style.css b/static/style.css index 8ef32c6..01c0910 100644 --- a/static/style.css +++ b/static/style.css @@ -1,9 +1 @@ -body { - font-size: 16px; - font-family: sans-serif; -} - -.error { - color: red; -} - +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.contents{display:contents}.hidden{display:none} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..4dea733 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./view/*.templ"], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/timer.db b/timer.db deleted file mode 100644 index 606b560..0000000 Binary files a/timer.db and /dev/null differ diff --git a/timer.go b/timer.go deleted file mode 100644 index 4b29726..0000000 --- a/timer.go +++ /dev/null @@ -1,323 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "fmt" - "log" - "net/http" - "strings" - - "golang.org/x/crypto/bcrypt" - - _ "github.com/mattn/go-sqlite3" - - "stevenlr.com/timer/model" - "stevenlr.com/timer/utils" - "stevenlr.com/timer/view" -) - -type TimerServer struct { - db *sql.DB - sessions Sessions -} - -func (server *TimerServer) findCurrentUser(w http.ResponseWriter, r *http.Request) *model.User { - return server.sessions.FindCurrentUser(server.db, w, r) -} - -func (server *TimerServer) handleNotFound(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - view.Error404().Render(context.Background(), w) -} - -func (server *TimerServer) handleMain(w http.ResponseWriter, r *http.Request) { - currentUser := server.findCurrentUser(w, r) - if r.URL.Path == "/" { - timers := make([]model.Timer, 0) - if currentUser != nil { - timers = model.GetTimersForUser(server.db, currentUser.Id) - } - view.Main(view.TimersList(timers, currentUser != nil), currentUser).Render(context.Background(), w) - } else { - server.handleNotFound(w, r) - } -} - -func (server *TimerServer) handleTimer(w http.ResponseWriter, r *http.Request) { - currentUser := server.findCurrentUser(w, r) - if currentUser == nil { - server.handleNotFound(w, r) - return - } - - var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { - server.handleNotFound(w, r) - return - } - - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer != nil && timer.Owner == currentUser.Id { - view.Main(view.TimerView(*timer), currentUser).Render(context.Background(), w) - } else { - server.handleNotFound(w, r) - } -} - -func (server *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *http.Request, timer *model.Timer) bool { - if timer.IsFinished() { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Timer already finished")) - return false - } - - duration, err := utils.ParseDuration(r.FormValue("timeToAdd")) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) - return false - } - - timer.EndTime.Add(duration) - res := model.UpdateTimerEndTime(server.db, timer.Id, timer.EndTime, timer.Owner) - if !res { - w.WriteHeader(http.StatusInternalServerError) - return false - } - - return true -} - -func (server *TimerServer) handleTimerAddTime(w http.ResponseWriter, r *http.Request) { - currentUser := server.findCurrentUser(w, r) - if currentUser == nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - if !server.handleTimerAddTimeCommon(w, r, timer) { - return - } - - view.TimerInfo(*timer).Render(context.Background(), w) -} - -func (server *TimerServer) handleApiTimerAddTime(w http.ResponseWriter, r *http.Request) { - var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - - timer := model.GetTimerWithToken(server.db, id, r.FormValue("token")) - if timer == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - if !server.handleTimerAddTimeCommon(w, r, timer) { - return - } -} - -func (server *TimerServer) handleGetTimerToken(w http.ResponseWriter, r *http.Request) { - currentUser := server.findCurrentUser(w, r) - if currentUser == nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer == nil { - server.handleNotFound(w, r) - return - } - - w.Write([]byte(fmt.Sprint("", timer.Token, ""))) -} - -func (server *TimerServer) handleResetTimerToken(w http.ResponseWriter, r *http.Request) { - currentUser := server.findCurrentUser(w, r) - if currentUser == nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - res := model.RegenerateTimerToken(server.db, timer.Id, currentUser.Id) - if !res { - w.WriteHeader(http.StatusInternalServerError) - return - } - - view.TimerTokenForm(*timer).Render(context.Background(), w) -} - -func (server *TimerServer) handleDeleteTimer(w http.ResponseWriter, r *http.Request) { - user := server.findCurrentUser(w, r) - if user == nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - - success := model.DeleteTimer(server.db, id, user.Id) - if !success { - w.WriteHeader(http.StatusNotFound) - } -} - -func (server *TimerServer) handleCreateTimer(w http.ResponseWriter, r *http.Request) { - timerName := strings.TrimSpace(r.FormValue("timerName")) - - user := server.findCurrentUser(w, r) - if user == nil { - w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm(timerName, "You are not signed in").Render(context.Background(), w) - return - } - - days, err := utils.ParseNumber(r.FormValue("days")) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm(timerName, "Error parsing days").Render(context.Background(), w) - return - } - - hours, err := utils.ParseNumber(r.FormValue("hours")) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm(timerName, "Error parsing hours").Render(context.Background(), w) - return - } - - tx, err := server.db.Begin() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - view.TimerCreateForm(timerName, "Internal server error").Render(context.Background(), w) - return - } - defer tx.Rollback() - - if timerName == "" { - w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm("", "Timer name cannot be empty").Render(context.Background(), w) - return - } - - err = model.InsertTimer(tx, timerName, int(((max(days, 0)*24)+max(hours, 0))*3600), user.Id) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - view.TimerCreateForm(timerName, "Internal server error").Render(context.Background(), w) - return - } - - tx.Commit() - - timers := model.GetTimersForUser(server.db, user.Id) - view.TimersList(timers, user != nil).Render(context.Background(), w) -} - -func (server *TimerServer) handlePostLogin(w http.ResponseWriter, r *http.Request) { - if server.findCurrentUser(w, r) != nil { - utils.HtmxRedirect(w, "/") - return - } - - userName := r.FormValue("user") - userPass := r.FormValue("password") - - user := model.GetUserByName(server.db, userName) - if user == nil { - w.WriteHeader(http.StatusBadRequest) - view.LoginFormError(nil, "Incorrect credentials").Render(context.Background(), w) - return - } - - err := bcrypt.CompareHashAndPassword(user.Password, []byte(userPass)) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - view.LoginFormError(nil, "Incorrect credentials").Render(context.Background(), w) - return - } - - if err := server.sessions.StartSession(user.Id, w); err == nil { - utils.HtmxRedirect(w, "/") - } else { - w.WriteHeader(http.StatusInternalServerError) - view.LoginFormError(nil, "Internal server error").Render(context.Background(), w) - } -} - -func (server *TimerServer) handlePostLogout(w http.ResponseWriter, r *http.Request) { - server.sessions.EndSession(w, r) - utils.HtmxRedirect(w, "/") -} - -func main() { - log.Println("Starting...") - - db, err := sql.Open("sqlite3", "file:timer.db") - if err != nil { - log.Fatalln(err) - } - defer db.Close() - - if err := InitializeDatabase(db); err != nil { - log.Fatalln(err) - } - - myServer := TimerServer{db: db, sessions: MakeSessions()} - - fs := http.FileServer(http.Dir("static/")) - http.Handle("GET /static/", http.StripPrefix("/static/", fs)) - - http.HandleFunc("POST /login", myServer.handlePostLogin) - http.HandleFunc("POST /logout", myServer.handlePostLogout) - http.HandleFunc("GET /timer/{timerId}", myServer.handleTimer) - http.HandleFunc("POST /timer/{timerId}/addTime", myServer.handleTimerAddTime) - http.HandleFunc("POST /api/timer/{timerId}/addTime", myServer.handleApiTimerAddTime) - http.HandleFunc("DELETE /timer/{timerId}", myServer.handleDeleteTimer) - http.HandleFunc("POST /timer/{timerId}/resetToken", myServer.handleResetTimerToken) - http.HandleFunc("GET /timer/{timerId}/token", myServer.handleGetTimerToken) - http.HandleFunc("PUT /timer", myServer.handleCreateTimer) - http.HandleFunc("GET /", myServer.handleMain) - - log.Println("Started!") - http.ListenAndServe("0.0.0.0:80", nil) -} diff --git a/view/lock.templ b/view/lock.templ new file mode 100644 index 0000000..4dd0516 --- /dev/null +++ b/view/lock.templ @@ -0,0 +1,80 @@ +package view + +import ( + "fmt" + "stevenlr.com/locker/model" +) + +templ LockTokenForm(lock model.Lock) { +

+ + +

+} + +func renderTimeString(value int, unit string) string { + s := "" + if value > 1 { s = "s" } + return fmt.Sprint(value, " ", unit, s) +} + +templ timeButton(id model.UUID, value int, unit string) { + + +} + +templ LockInfo(lock model.Lock) { +

Lock "{ lock.Name }"

+

Start time: { lock.StartTime.AsUTCString() }

+

End time: { lock.EndTime.AsUTCString() }

+

+ Total time: + +

+

+ Remaining time: + +

+} + +templ LockView(lock model.Lock) { +

Back to list

+
+ @LockInfo(lock) +
+ if !lock.IsFinished() { +

Add time

+

+ @timeButton(lock.Id, 15, "minute") + @timeButton(lock.Id, 30, "minute") + @timeButton(lock.Id, 1, "hour") + @timeButton(lock.Id, 2, "hour") + @timeButton(lock.Id, 6, "hour") + @timeButton(lock.Id, 12, "hour") + @timeButton(lock.Id, 1, "day") + @timeButton(lock.Id, 1, "week") + @timeButton(lock.Id, 4, "week") +

+ } +

API token

+ @LockTokenForm(lock) +} + diff --git a/view/locks_list.templ b/view/locks_list.templ new file mode 100644 index 0000000..e940b88 --- /dev/null +++ b/view/locks_list.templ @@ -0,0 +1,51 @@ +package view + +import ( + "stevenlr.com/locker/model" + "fmt" +) + +templ lock(t model.Lock) { +

+ { t.Name } + - + Delete +

+} + +templ LockCreateForm(lockName string, err string) { +
+

+ + + + +

+ if err != "" { +

{ err }

+ } +
+} + +templ LocksList(locks []model.Lock, isSignedIn bool) { +
+ if isSignedIn { +

Locks

+ for _, t := range locks { + @lock(t) + } +

Create lock

+ @LockCreateForm("", "") + } +
+} + diff --git a/view/login.templ b/view/login.templ index 3450e01..56e003c 100644 --- a/view/login.templ +++ b/view/login.templ @@ -1,7 +1,7 @@ package view import ( - "stevenlr.com/timer/model" + "stevenlr.com/locker/model" ) templ LoginFormError(currentUser *model.User, err string) { diff --git a/view/main.templ b/view/main.templ index 06cb872..4ae917b 100644 --- a/view/main.templ +++ b/view/main.templ @@ -1,14 +1,14 @@ package view import ( - "stevenlr.com/timer/model" + "stevenlr.com/locker/model" ) templ Main(contents templ.Component, currentUser *model.User) { - Cool timer app + Cool locker app diff --git a/view/timer.templ b/view/timer.templ deleted file mode 100644 index 09b1345..0000000 --- a/view/timer.templ +++ /dev/null @@ -1,80 +0,0 @@ -package view - -import ( - "fmt" - "stevenlr.com/timer/model" -) - -templ TimerTokenForm(timer model.Timer) { -

- - -

-} - -func renderTimeString(value int, unit string) string { - s := "" - if value > 1 { s = "s" } - return fmt.Sprint(value, " ", unit, s) -} - -templ timeButton(id model.UUID, value int, unit string) { - - -} - -templ TimerInfo(timer model.Timer) { -

Timer "{ timer.Name }"

-

Start time: { timer.StartTime.AsUTCString() }

-

End time: { timer.EndTime.AsUTCString() }

-

- Total time: - -

-

- Remaining time: - -

-} - -templ TimerView(timer model.Timer) { -

Back to list

-
- @TimerInfo(timer) -
- if !timer.IsFinished() { -

Add time

-

- @timeButton(timer.Id, 15, "minute") - @timeButton(timer.Id, 30, "minute") - @timeButton(timer.Id, 1, "hour") - @timeButton(timer.Id, 2, "hour") - @timeButton(timer.Id, 6, "hour") - @timeButton(timer.Id, 12, "hour") - @timeButton(timer.Id, 1, "day") - @timeButton(timer.Id, 1, "week") - @timeButton(timer.Id, 4, "week") -

- } -

API token

- @TimerTokenForm(timer) -} - diff --git a/view/timers_list.templ b/view/timers_list.templ deleted file mode 100644 index 69a8e53..0000000 --- a/view/timers_list.templ +++ /dev/null @@ -1,51 +0,0 @@ -package view - -import ( - "stevenlr.com/timer/model" - "fmt" -) - -templ timer(t model.Timer) { -

- { t.Name } - - - Delete -

-} - -templ TimerCreateForm(timerName string, err string) { -
-

- - - - -

- if err != "" { -

{ err }

- } -
-} - -templ TimersList(timers []model.Timer, isSignedIn bool) { -
- if isSignedIn { -

Timers

- for _, t := range timers { - @timer(t) - } -

Create timer

- @TimerCreateForm("", "") - } -
-} - -- cgit