Rename everything to locker/lock
This commit is contained in:
4
build.bat
Normal file
4
build.bat
Normal file
@ -0,0 +1,4 @@
|
||||
templ generate
|
||||
tailwindcss -i css\input.css -o static\style.css --minify
|
||||
|
||||
|
3
css/input.css
Normal file
3
css/input.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -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
|
||||
}
|
||||
|
Binary file not shown.
@ -12,39 +12,39 @@ import (
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"stevenlr.com/timer/model"
|
||||
"stevenlr.com/timer/utils"
|
||||
"stevenlr.com/timer/view"
|
||||
"stevenlr.com/locker/model"
|
||||
"stevenlr.com/locker/utils"
|
||||
"stevenlr.com/locker/view"
|
||||
)
|
||||
|
||||
type TimerServer struct {
|
||||
type LockServer struct {
|
||||
db *sql.DB
|
||||
sessions Sessions
|
||||
}
|
||||
|
||||
func (server *TimerServer) findCurrentUser(w http.ResponseWriter, r *http.Request) *model.User {
|
||||
func (server *LockServer) 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) {
|
||||
func (server *LockServer) 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) {
|
||||
func (server *LockServer) handleMain(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := server.findCurrentUser(w, r)
|
||||
if r.URL.Path == "/" {
|
||||
timers := make([]model.Timer, 0)
|
||||
locks := make([]model.Lock, 0)
|
||||
if currentUser != nil {
|
||||
timers = model.GetTimersForUser(server.db, currentUser.Id)
|
||||
locks = model.GetLocksForUser(server.db, currentUser.Id)
|
||||
}
|
||||
view.Main(view.TimersList(timers, currentUser != nil), currentUser).Render(context.Background(), w)
|
||||
view.Main(view.LocksList(locks, currentUser != nil), currentUser).Render(context.Background(), w)
|
||||
} else {
|
||||
server.handleNotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *TimerServer) handleTimer(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handleLock(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := server.findCurrentUser(w, r)
|
||||
if currentUser == nil {
|
||||
server.handleNotFound(w, r)
|
||||
@ -52,23 +52,23 @@ func (server *TimerServer) handleTimer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var id model.UUID
|
||||
if err := id.Scan(r.PathValue("timerId")); err != nil {
|
||||
if err := id.Scan(r.PathValue("lockId")); 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)
|
||||
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 *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *http.Request, timer *model.Timer) bool {
|
||||
if timer.IsFinished() {
|
||||
func (server *LockServer) handleLockAddTimeCommon(w http.ResponseWriter, r *http.Request, lock *model.Lock) bool {
|
||||
if lock.IsFinished() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Timer already finished"))
|
||||
w.Write([]byte("Lock already finished"))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -79,8 +79,8 @@ func (server *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *ht
|
||||
return false
|
||||
}
|
||||
|
||||
timer.EndTime.Add(duration)
|
||||
res := model.UpdateTimerEndTime(server.db, timer.Id, timer.EndTime, timer.Owner)
|
||||
lock.EndTime.Add(duration)
|
||||
res := model.UpdateLockEndTime(server.db, lock.Id, lock.EndTime, lock.Owner)
|
||||
if !res {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
@ -89,7 +89,7 @@ func (server *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *ht
|
||||
return true
|
||||
}
|
||||
|
||||
func (server *TimerServer) handleTimerAddTime(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handleLockAddTime(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := server.findCurrentUser(w, r)
|
||||
if currentUser == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@ -97,43 +97,43 @@ func (server *TimerServer) handleTimerAddTime(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
var id model.UUID
|
||||
if err := id.Scan(r.PathValue("timerId")); err != nil {
|
||||
if err := id.Scan(r.PathValue("lockId")); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
timer := model.GetTimerForUser(server.db, id, currentUser.Id)
|
||||
if timer == nil {
|
||||
lock := model.GetLockForUser(server.db, id, currentUser.Id)
|
||||
if lock == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !server.handleTimerAddTimeCommon(w, r, timer) {
|
||||
if !server.handleLockAddTimeCommon(w, r, lock) {
|
||||
return
|
||||
}
|
||||
|
||||
view.TimerInfo(*timer).Render(context.Background(), w)
|
||||
view.LockInfo(*lock).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
func (server *TimerServer) handleApiTimerAddTime(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handleApiLockAddTime(w http.ResponseWriter, r *http.Request) {
|
||||
var id model.UUID
|
||||
if err := id.Scan(r.PathValue("timerId")); err != nil {
|
||||
if err := id.Scan(r.PathValue("lockId")); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
timer := model.GetTimerWithToken(server.db, id, r.FormValue("token"))
|
||||
if timer == nil {
|
||||
lock := model.GetLockWithToken(server.db, id, r.FormValue("token"))
|
||||
if lock == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !server.handleTimerAddTimeCommon(w, r, timer) {
|
||||
if !server.handleLockAddTimeCommon(w, r, lock) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (server *TimerServer) handleGetTimerToken(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handleGetLockToken(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := server.findCurrentUser(w, r)
|
||||
if currentUser == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@ -141,21 +141,21 @@ func (server *TimerServer) handleGetTimerToken(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
var id model.UUID
|
||||
if err := id.Scan(r.PathValue("timerId")); err != nil {
|
||||
if err := id.Scan(r.PathValue("lockId")); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
timer := model.GetTimerForUser(server.db, id, currentUser.Id)
|
||||
if timer == nil {
|
||||
lock := model.GetLockForUser(server.db, id, currentUser.Id)
|
||||
if lock == nil {
|
||||
server.handleNotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(fmt.Sprint("<code>", timer.Token, "</code>")))
|
||||
w.Write([]byte(fmt.Sprint("<code>", lock.Token, "</code>")))
|
||||
}
|
||||
|
||||
func (server *TimerServer) handleResetTimerToken(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handleResetLockToken(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := server.findCurrentUser(w, r)
|
||||
if currentUser == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@ -163,27 +163,27 @@ func (server *TimerServer) handleResetTimerToken(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
var id model.UUID
|
||||
if err := id.Scan(r.PathValue("timerId")); err != nil {
|
||||
if err := id.Scan(r.PathValue("lockId")); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
timer := model.GetTimerForUser(server.db, id, currentUser.Id)
|
||||
if timer == nil {
|
||||
lock := model.GetLockForUser(server.db, id, currentUser.Id)
|
||||
if lock == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
res := model.RegenerateTimerToken(server.db, timer.Id, currentUser.Id)
|
||||
res := model.RegenerateLockToken(server.db, lock.Id, currentUser.Id)
|
||||
if !res {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
view.TimerTokenForm(*timer).Render(context.Background(), w)
|
||||
view.LockTokenForm(*lock).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
func (server *TimerServer) handleDeleteTimer(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handleDeleteLock(w http.ResponseWriter, r *http.Request) {
|
||||
user := server.findCurrentUser(w, r)
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@ -191,69 +191,69 @@ func (server *TimerServer) handleDeleteTimer(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
var id model.UUID
|
||||
if err := id.Scan(r.PathValue("timerId")); err != nil {
|
||||
if err := id.Scan(r.PathValue("lockId")); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
success := model.DeleteTimer(server.db, id, user.Id)
|
||||
success := model.DeleteLock(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"))
|
||||
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.TimerCreateForm(timerName, "You are not signed in").Render(context.Background(), w)
|
||||
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.TimerCreateForm(timerName, "Error parsing days").Render(context.Background(), w)
|
||||
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.TimerCreateForm(timerName, "Error parsing hours").Render(context.Background(), w)
|
||||
view.LockCreateForm(lockName, "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)
|
||||
view.LockCreateForm(lockName, "Internal server error").Render(context.Background(), w)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if timerName == "" {
|
||||
if lockName == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
view.TimerCreateForm("", "Timer name cannot be empty").Render(context.Background(), w)
|
||||
view.LockCreateForm("", "Lock 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)
|
||||
err = model.InsertLock(tx, lockName, 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)
|
||||
view.LockCreateForm(lockName, "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)
|
||||
locks := model.GetLocksForUser(server.db, user.Id)
|
||||
view.LocksList(locks, user != nil).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
func (server *TimerServer) handlePostLogin(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handlePostLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if server.findCurrentUser(w, r) != nil {
|
||||
utils.HtmxRedirect(w, "/")
|
||||
return
|
||||
@ -284,7 +284,7 @@ func (server *TimerServer) handlePostLogin(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
func (server *TimerServer) handlePostLogout(w http.ResponseWriter, r *http.Request) {
|
||||
func (server *LockServer) handlePostLogout(w http.ResponseWriter, r *http.Request) {
|
||||
server.sessions.EndSession(w, r)
|
||||
utils.HtmxRedirect(w, "/")
|
||||
}
|
||||
@ -292,7 +292,7 @@ func (server *TimerServer) handlePostLogout(w http.ResponseWriter, r *http.Reque
|
||||
func main() {
|
||||
log.Println("Starting...")
|
||||
|
||||
db, err := sql.Open("sqlite3", "file:timer.db")
|
||||
db, err := sql.Open("sqlite3", "file:lock.db")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
@ -302,20 +302,20 @@ func main() {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
myServer := TimerServer{db: db, sessions: MakeSessions()}
|
||||
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 /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 /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!")
|
110
model/lock.go
Normal file
110
model/lock.go
Normal file
@ -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
|
||||
}
|
110
model/timer.go
110
model/timer.go
@ -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
|
||||
}
|
@ -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{
|
||||
|
@ -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}
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./view/*.templ"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
80
view/lock.templ
Normal file
80
view/lock.templ
Normal file
@ -0,0 +1,80 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"stevenlr.com/locker/model"
|
||||
)
|
||||
|
||||
templ LockTokenForm(lock model.Lock) {
|
||||
<p class="token-form">
|
||||
<button
|
||||
type="button"
|
||||
hx-post={ fmt.Sprint("/lock/", lock.Id, "/resetToken") }
|
||||
hx-target="closest .token-form"
|
||||
hx-confirm="Are you sure you want to reset the token for this lock?"
|
||||
>Reset token</button>
|
||||
<button
|
||||
type="button"
|
||||
hx-get={ fmt.Sprint("/lock/", lock.Id, "/token") }
|
||||
hx-swap="outerHTML"
|
||||
>Show token</button>
|
||||
</p>
|
||||
}
|
||||
|
||||
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) {
|
||||
<button
|
||||
hx-target=".lock-info"
|
||||
hx-post={ fmt.Sprint("/lock/", id, "/addTime") }
|
||||
hx-include="next input"
|
||||
>{ renderTimeString(value, unit) }</button>
|
||||
<input type="hidden" name="timeToAdd" value={ fmt.Sprint(value, "", unit[:1]) } />
|
||||
}
|
||||
|
||||
templ LockInfo(lock model.Lock) {
|
||||
<h1>Lock "{ lock.Name }"</h1>
|
||||
<p>Start time: <local-date>{ lock.StartTime.AsUTCString() }</local-date></p>
|
||||
<p>End time: <local-date>{ lock.EndTime.AsUTCString() }</local-date></p>
|
||||
<p>
|
||||
Total time:
|
||||
<lock-countdown
|
||||
start={ lock.StartTime.AsUTCString() }
|
||||
end={ lock.EndTime.AsUTCString() }
|
||||
></lock-countdown>
|
||||
</p>
|
||||
<p>
|
||||
Remaining time:
|
||||
<lock-countdown
|
||||
end={ lock.EndTime.AsUTCString() }
|
||||
></lock-countdown>
|
||||
</p>
|
||||
}
|
||||
|
||||
templ LockView(lock model.Lock) {
|
||||
<p><a href="/">Back to list</a></p>
|
||||
<div class="lock-info">
|
||||
@LockInfo(lock)
|
||||
</div>
|
||||
if !lock.IsFinished() {
|
||||
<h3>Add time</h3>
|
||||
<p>
|
||||
@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")
|
||||
</p>
|
||||
}
|
||||
<h3>API token</h3>
|
||||
@LockTokenForm(lock)
|
||||
}
|
||||
|
51
view/locks_list.templ
Normal file
51
view/locks_list.templ
Normal file
@ -0,0 +1,51 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"stevenlr.com/locker/model"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ lock(t model.Lock) {
|
||||
<p class="lock-row">
|
||||
<a href={ templ.URL(fmt.Sprint("/lock/", t.Id)) }>{ t.Name }</a>
|
||||
-
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
hx-delete={ fmt.Sprint("/lock/", t.Id) }
|
||||
hx-target="closest .lock-row"
|
||||
hx-confirm={ fmt.Sprint("Are you sure you want to delete lock \"", t.Name , "\"?") }
|
||||
>Delete</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
templ LockCreateForm(lockName string, err string) {
|
||||
<form
|
||||
hx-put="/lock"
|
||||
hx-target="closest .locks-list"
|
||||
hx-target-error="this"
|
||||
>
|
||||
<p>
|
||||
<input type="text" name="lockName" value={ lockName } placeholder="Name" />
|
||||
<input type="number" name="days" placeholder="Days" />
|
||||
<input type="number" name="hours" placeholder="Hours" />
|
||||
<button type="submit">Create</button>
|
||||
</p>
|
||||
if err != "" {
|
||||
<p class="error">{ err }</p>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
|
||||
templ LocksList(locks []model.Lock, isSignedIn bool) {
|
||||
<div class="locks-list">
|
||||
if isSignedIn {
|
||||
<h1>Locks</h1>
|
||||
for _, t := range locks {
|
||||
@lock(t)
|
||||
}
|
||||
<h4>Create lock</h4>
|
||||
@LockCreateForm("", "")
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"stevenlr.com/timer/model"
|
||||
"stevenlr.com/locker/model"
|
||||
)
|
||||
|
||||
templ LoginFormError(currentUser *model.User, err string) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"stevenlr.com/timer/model"
|
||||
"stevenlr.com/locker/model"
|
||||
)
|
||||
|
||||
templ Main(contents templ.Component, currentUser *model.User) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Cool timer app</title>
|
||||
<title>Cool locker app</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<script type="module" src="/static/ui-components.js"></script>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
|
@ -1,80 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"stevenlr.com/timer/model"
|
||||
)
|
||||
|
||||
templ TimerTokenForm(timer model.Timer) {
|
||||
<p class="token-form">
|
||||
<button
|
||||
type="button"
|
||||
hx-post={ fmt.Sprint("/timer/", timer.Id, "/resetToken") }
|
||||
hx-target="closest .token-form"
|
||||
hx-confirm="Are you sure you want to reset the token for this timer?"
|
||||
>Reset token</button>
|
||||
<button
|
||||
type="button"
|
||||
hx-get={ fmt.Sprint("/timer/", timer.Id, "/token") }
|
||||
hx-swap="outerHTML"
|
||||
>Show token</button>
|
||||
</p>
|
||||
}
|
||||
|
||||
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) {
|
||||
<button
|
||||
hx-target=".timer-info"
|
||||
hx-post={ fmt.Sprint("/timer/", id, "/addTime") }
|
||||
hx-include="next input"
|
||||
>{ renderTimeString(value, unit) }</button>
|
||||
<input type="hidden" name="timeToAdd" value={ fmt.Sprint(value, "", unit[:1]) } />
|
||||
}
|
||||
|
||||
templ TimerInfo(timer model.Timer) {
|
||||
<h1>Timer "{ timer.Name }"</h1>
|
||||
<p>Start time: <local-date>{ timer.StartTime.AsUTCString() }</local-date></p>
|
||||
<p>End time: <local-date>{ timer.EndTime.AsUTCString() }</local-date></p>
|
||||
<p>
|
||||
Total time:
|
||||
<timer-countdown
|
||||
start={ timer.StartTime.AsUTCString() }
|
||||
end={ timer.EndTime.AsUTCString() }
|
||||
></timer-countdown>
|
||||
</p>
|
||||
<p>
|
||||
Remaining time:
|
||||
<timer-countdown
|
||||
end={ timer.EndTime.AsUTCString() }
|
||||
></timer-countdown>
|
||||
</p>
|
||||
}
|
||||
|
||||
templ TimerView(timer model.Timer) {
|
||||
<p><a href="/">Back to list</a></p>
|
||||
<div class="timer-info">
|
||||
@TimerInfo(timer)
|
||||
</div>
|
||||
if !timer.IsFinished() {
|
||||
<h3>Add time</h3>
|
||||
<p>
|
||||
@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")
|
||||
</p>
|
||||
}
|
||||
<h3>API token</h3>
|
||||
@TimerTokenForm(timer)
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"stevenlr.com/timer/model"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ timer(t model.Timer) {
|
||||
<p class="timer-row">
|
||||
<a href={ templ.URL(fmt.Sprint("/timer/", t.Id)) }>{ t.Name }</a>
|
||||
-
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
hx-delete={ fmt.Sprint("/timer/", t.Id) }
|
||||
hx-target="closest .timer-row"
|
||||
hx-confirm={ fmt.Sprint("Are you sure you want to delete timer \"", t.Name , "\"?") }
|
||||
>Delete</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
templ TimerCreateForm(timerName string, err string) {
|
||||
<form
|
||||
hx-put="/timer"
|
||||
hx-target="closest .timers-list"
|
||||
hx-target-error="this"
|
||||
>
|
||||
<p>
|
||||
<input type="text" name="timerName" value={ timerName } placeholder="Name" />
|
||||
<input type="number" name="days" placeholder="Days" style="width: 5em;" />
|
||||
<input type="number" name="hours" placeholder="Hours" style="width: 5em;" />
|
||||
<button type="submit">Create</button>
|
||||
</p>
|
||||
if err != "" {
|
||||
<p class="error">{ err }</p>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
|
||||
templ TimersList(timers []model.Timer, isSignedIn bool) {
|
||||
<div class="timers-list">
|
||||
if isSignedIn {
|
||||
<h1>Timers</h1>
|
||||
for _, t := range timers {
|
||||
@timer(t)
|
||||
}
|
||||
<h4>Create timer</h4>
|
||||
@TimerCreateForm("", "")
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
Reference in New Issue
Block a user