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) }