User login & logout
This commit is contained in:
8
model/user.go
Normal file
8
model/user.go
Normal file
@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
Id UUID
|
||||
Name string
|
||||
Salt string
|
||||
Password []byte
|
||||
}
|
122
timer.go
122
timer.go
@ -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)
|
||||
|
29
view/login.templ
Normal file
29
view/login.templ
Normal file
@ -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, "")
|
||||
}
|
||||
|
114
view/login_templ.go
Normal file
114
view/login_templ.go
Normal file
@ -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
|
||||
})
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user