diff options
-rw-r--r-- | model/user.go | 8 | ||||
-rw-r--r-- | timer.go | 122 | ||||
-rw-r--r-- | view/login.templ | 29 | ||||
-rw-r--r-- | view/login_templ.go | 114 | ||||
-rw-r--r-- | view/main.templ | 7 | ||||
-rw-r--r-- | view/main_templ.go | 10 |
6 files changed, 282 insertions, 8 deletions
diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..4959371 --- /dev/null +++ b/model/user.go @@ -0,0 +1,8 @@ +package model + +type User struct { + Id UUID + Name string + Salt string + Password []byte +} @@ -12,6 +12,8 @@ import ( "golang.org/x/crypto/bcrypt" + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" "stevenlr.com/timer/model" @@ -51,12 +53,12 @@ func initializeDatabase(db *sql.DB) error { return err } - err = insertTimer(tx, "My timer", 6) + err = insertTimer(tx, "My timer", 600) if err != nil { return err } - err = insertTimer(tx, "My timer2", 6) + err = insertTimer(tx, "My timer2", 600) if err != nil { return err } @@ -112,6 +114,30 @@ func queryAllTimers(db *sql.DB) []model.Timer { return timers } +func queryUserByName(db *sql.DB, name string) *model.User { + row := db.QueryRow("SELECT Id, Name, Salt, Password FROM User WHERE Name=$1", name) + if row == nil { + return nil + } + + var user model.User + row.Scan(&user.Id, &user.Name, &user.Salt, &user.Password) + + return &user +} + +func queryUserById(db *sql.DB, id model.UUID) *model.User { + row := db.QueryRow("SELECT Id, Name, Salt, Password FROM User WHERE Id=$1", id) + if row == nil { + return nil + } + + var user model.User + row.Scan(&user.Id, &user.Name, &user.Salt, &user.Password) + + return &user +} + func queryTimer(db *sql.DB, idStr string) *model.Timer { var id model.UUID if err := id.Scan(idStr); err != nil { @@ -154,7 +180,7 @@ func updateTimerEndTime(db *sql.DB, id model.UUID, endTime model.Time) bool { } type Session struct { - UserId []byte + UserId model.UUID } type MyServer struct { @@ -162,24 +188,57 @@ type MyServer struct { sessions map[string]Session } +const SessionCookieName = "timerSession" + +func removeCookie(cookieName string, w http.ResponseWriter) { + cookie := http.Cookie{ + Name: cookieName, + Value: "", + MaxAge: -1, + } + http.SetCookie(w, &cookie) +} + +func (server *MyServer) findCurrentUser(w http.ResponseWriter, r *http.Request) *model.User { + cookie, err := r.Cookie(SessionCookieName) + if err != nil { + return nil + } + + userId, ok := server.sessions[cookie.Value] + if !ok { + removeCookie(SessionCookieName, w) + return nil + } + + user := queryUserById(server.db, userId.UserId) + if user == nil { + removeCookie(SessionCookieName, w) + } + + return user +} + func (server *MyServer) handleNotFound(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) - view.Main(view.Error404()).Render(context.Background(), w) + view.Error404().Render(context.Background(), w) } func (server *MyServer) handleMain(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) if r.URL.Path == "/" { timers := queryAllTimers(server.db) - view.Main(view.TimersList(timers)).Render(context.Background(), w) + view.Main(view.TimersList(timers), currentUser).Render(context.Background(), w) } else { server.handleNotFound(w, r) } } func (server *MyServer) handleTimer(w http.ResponseWriter, r *http.Request) { + currentUser := server.findCurrentUser(w, r) timer := queryTimer(server.db, r.PathValue("timerId")) if timer != nil { - view.Main(view.TimerView(*timer)).Render(context.Background(), w) + view.Main(view.TimerView(*timer), currentUser).Render(context.Background(), w) } else { server.handleNotFound(w, r) } @@ -295,6 +354,55 @@ func (server *MyServer) handlePutTimer(w http.ResponseWriter, r *http.Request) { view.TimersList(timers).Render(context.Background(), w) } +func (server *MyServer) handlePostLogin(w http.ResponseWriter, r *http.Request) { + if server.findCurrentUser(w, r) != nil { + w.Header().Add("HX-Redirect", "/") + return + } + + userName := r.FormValue("user") + userPass := r.FormValue("password") + + user := queryUserByName(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(user.Salt+userPass)) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + view.LoginFormError(nil, "Incorrect credentials").Render(context.Background(), w) + return + } + + sessionId, err := uuid.NewRandom() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + view.LoginFormError(nil, "Internal server error").Render(context.Background(), w) + return + } + + cookie := http.Cookie{ + Name: SessionCookieName, + Value: sessionId.String(), + HttpOnly: true, + Secure: true, + } + server.sessions[sessionId.String()] = Session{UserId: user.Id} + http.SetCookie(w, &cookie) + w.Header().Add("HX-Redirect", "/") +} + +func (server *MyServer) handlePostLogout(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie(SessionCookieName); err == nil { + delete(server.sessions, cookie.Value) + removeCookie(SessionCookieName, w) + } + w.Header().Add("HX-Redirect", "/") +} + func main() { log.Println("Starting...") @@ -313,6 +421,8 @@ func main() { 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/{timeToAdd}", myServer.handleTimerAddTime) http.HandleFunc("DELETE /timer/{timerId}", myServer.handleDeleteTimer) diff --git a/view/login.templ b/view/login.templ new file mode 100644 index 0000000..f8ea6e3 --- /dev/null +++ b/view/login.templ @@ -0,0 +1,29 @@ +package view
+
+import (
+ "stevenlr.com/timer/model"
+)
+
+templ LoginFormError(currentUser *model.User, err string) {
+ <div class="login-form">
+ if currentUser == nil {
+ <form hx-post="/login" hx-target-error="closest .login-form">
+ <p>
+ <input type="text" name="user" placeholder="User" />
+ <input type="password" name="password" placeholder="Password" />
+ <button type="submit">Sign in</button>
+ if err != "" {
+ <span style="color:red;">{ err }</span>
+ }
+ </p>
+ </form>
+ } else {
+ <p>Signed in as { currentUser.Name } <button type="button" hx-post="/logout" hx-refresh>Sign out</button></p>
+ }
+ </div>
+}
+
+templ LoginForm(currentUser *model.User) {
+ @LoginFormError(currentUser, "")
+}
+
diff --git a/view/login_templ.go b/view/login_templ.go new file mode 100644 index 0000000..db3d53e --- /dev/null +++ b/view/login_templ.go @@ -0,0 +1,114 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.648 +package view + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import ( + "stevenlr.com/timer/model" +) + +func LoginFormError(currentUser *model.User, err string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + 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("<div class=\"login-form\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if currentUser == nil { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form hx-post=\"/login\" hx-target-error=\"closest .login-form\"><p><input type=\"text\" name=\"user\" placeholder=\"User\"> <input type=\"password\" name=\"password\" placeholder=\"Password\"> <button type=\"submit\">Sign in</button> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if err != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span style=\"color:red;\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(err) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\login.templ`, Line: 16, Col: 50} + } + _, 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("</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></form>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>Signed in as ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(currentUser.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view\login.templ`, Line: 21, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <button type=\"button\" hx-post=\"/logout\" hx-refresh>Sign out</button></p>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func LoginForm(currentUser *model.User) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + 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_Err = LoginFormError(currentUser, "").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/view/main.templ b/view/main.templ index 64a365c..06cb872 100644 --- a/view/main.templ +++ b/view/main.templ @@ -1,6 +1,10 @@ package view
-templ Main(contents templ.Component) {
+import (
+ "stevenlr.com/timer/model"
+)
+
+templ Main(contents templ.Component, currentUser *model.User) {
<!DOCTYPE html>
<html>
<head>
@@ -11,6 +15,7 @@ templ Main(contents templ.Component) { <script src="/static/response-targets.js"></script>
</head>
<body hx-boost="true" hx-ext="response-targets">
+ @LoginForm(currentUser)
@contents
</body>
</html>
diff --git a/view/main_templ.go b/view/main_templ.go index 5b3831d..0b4504c 100644 --- a/view/main_templ.go +++ b/view/main_templ.go @@ -10,7 +10,11 @@ import "context" import "io" import "bytes" -func Main(contents templ.Component) templ.Component { +import ( + "stevenlr.com/timer/model" +) + +func Main(contents templ.Component, currentUser *model.User) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -27,6 +31,10 @@ func Main(contents templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + templ_7745c5c3_Err = LoginForm(currentUser).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = contents.Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err |