feat: list users json endpoint

This commit is contained in:
Teajey 2024-11-20 22:52:10 +09:00
parent d2fb0fa707
commit fac92511ee
Signed by: Teajey
GPG Key ID: 970E790FE834A713
15 changed files with 121 additions and 50 deletions

View File

@ -17,6 +17,8 @@ type RegisterProps struct {
} }
func (p *RegisterProps) Validate() (valid bool) { func (p *RegisterProps) Validate() (valid bool) {
valid = true
if p.Password.Value != p.ConfirmPassword.Value { if p.Password.Value != p.ConfirmPassword.Value {
p.ConfirmPassword.Error = "Passwords didn't match" p.ConfirmPassword.Error = "Passwords didn't match"
valid = false valid = false
@ -69,24 +71,27 @@ func Register(username, newPassword, confirmPassword string) *RegisterProps {
props.Password.Value = "" props.Password.Value = ""
props.ConfirmPassword.Value = "" props.ConfirmPassword.Value = ""
if !valid { if !valid {
log.Printf("Invalid props: %#v\n", props)
return props return props
} }
existingUser, _ := db.GetUserByName(username) existingUser, _ := db.GetUserByName(username)
if existingUser != nil { if existingUser != nil {
log.Printf("Username is taken: %q\n", username)
props.Username.Error = "Username is taken" props.Username.Error = "Username is taken"
return props return props
} }
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
if err != nil { if err != nil {
log.Printf("Failed to hash password: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Aang" props.GeneralError = "Something went wrong. Error code: Aang"
return props return props
} }
_, err = db.CreateUser(username, hashedPasswordBytes) _, err = db.CreateUser(username, hashedPasswordBytes)
if err != nil { if err != nil {
log.Println("Registration error:", err) log.Printf("Failed to create user: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Ozai" props.GeneralError = "Something went wrong. Error code: Ozai"
return props return props
} }

View File

@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS "user" (
"reference" TEXT NOT NULL UNIQUE, "reference" TEXT NOT NULL UNIQUE,
"motto" TEXT NOT NULL, "motto" TEXT NOT NULL,
"password_hash" TEXT NOT NULL, "password_hash" TEXT NOT NULL,
"is_admin" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT) PRIMARY KEY("id" AUTOINCREMENT)
); );
CREATE TABLE IF NOT EXISTS "gift" ( CREATE TABLE IF NOT EXISTS "gift" (

View File

@ -11,6 +11,7 @@ type User struct {
Id string Id string
Name string Name string
Reference string Reference string
IsAdmin bool
} }
type Gift struct { type Gift struct {
@ -27,30 +28,46 @@ type Gift struct {
} }
func queryForUser(query string, args ...any) (*User, error) { func queryForUser(query string, args ...any) (*User, error) {
var id string var u User
var name string err := database.QueryRow(query, args...).Scan(&u.Id, &u.Name, &u.Reference, &u.IsAdmin)
var reference string
err := database.QueryRow(query, args...).Scan(&id, &name, &reference)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
user := User{ return &u, nil
Id: id, }
Name: name,
Reference: reference, func GetAllUsers() ([]User, error) {
stmt := "SELECT user.id, user.name, user.reference, user.is_admin FROM user"
rows, err := database.Query(stmt)
if err != nil {
return nil, err
} }
return &user, nil defer rows.Close()
users := []User{}
for rows.Next() {
var u User
err = rows.Scan(&u.Id, &u.Name, &u.Reference, &u.IsAdmin)
if err != nil {
return nil, err
}
users = append(users, u)
}
err = rows.Err()
if err != nil {
return nil, err
}
return users, nil
} }
func GetUserByName(username string) (*User, error) { func GetUserByName(username string) (*User, error) {
stmt := "SELECT user.id, user.name, user.reference FROM user WHERE user.name = ?" stmt := "SELECT user.id, user.name, user.reference, user.is_admin FROM user WHERE user.name = ?"
return queryForUser(stmt, username) return queryForUser(stmt, username)
} }
func GetUserByReference(reference string) (*User, error) { func GetUserByReference(reference string) (*User, error) {
stmt := "SELECT user.id, user.name, user.reference FROM user WHERE user.reference = ?" stmt := "SELECT user.id, user.name, user.reference, user.is_admin FROM user WHERE user.reference = ?"
return queryForUser(stmt, reference) return queryForUser(stmt, reference)
} }

View File

@ -27,7 +27,7 @@ func main() {
store, err := db.NewSessionStore() store, err := db.NewSessionStore()
if err != nil { if err != nil {
log.Fatalf("Failed to ") log.Fatalf("Failed to initialize session store: %s\n", err)
} }
store.Options.MaxAge = 86_400 store.Options.MaxAge = 86_400
store.Options.Secure = !env.InDev store.Options.Secure = !env.InDev
@ -44,14 +44,17 @@ func main() {
r.Html.Public.HandleFunc("GET /list/{userReference}", route.PublicWishlist) r.Html.Public.HandleFunc("GET /list/{userReference}", route.PublicWishlist)
r.Html.Public.HandleFunc("GET /group/{groupReference}", route.PublicGroupPage) r.Html.Public.HandleFunc("GET /group/{groupReference}", route.PublicGroupPage)
r.Html.Private.HandleFunc("GET /{$}", route.Home) r.Html.Private.Handle("GET /{$}", route.ExpectUser(route.Home))
r.Html.Private.HandleFunc("POST /{$}", route.HomePost) r.Html.Private.Handle("POST /{$}", route.ExpectUser(route.HomePost))
r.Html.Private.HandleFunc("GET /list/{userReference}", route.ForeignWishlist) r.Html.Private.Handle("GET /list/{userReference}", route.ExpectUser(route.ForeignWishlist))
r.Html.Private.HandleFunc("POST /list/{userReference}", route.ForeignWishlistPost) r.Html.Private.Handle("POST /list/{userReference}", route.ExpectUser(route.ForeignWishlistPost))
r.Html.Private.HandleFunc("GET /group/{groupReference}", route.GroupPage) r.Html.Private.Handle("GET /group/{groupReference}", route.ExpectUser(route.GroupPage))
r.Html.Private.HandleFunc("POST /logout", route.LogoutPost) r.Html.Private.HandleFunc("POST /logout", route.LogoutPost)
r.Json.Public.HandleFunc("POST /register", route.RegisterPostJson) r.Json.Public.HandleFunc("POST /register", route.RegisterPostJson)
r.Json.Public.HandleFunc("GET /", routing.NotFoundJson)
r.Json.Private.Handle("GET /users", route.ExpectUser(route.UsersJson))
http.Handle("/", r) http.Handle("/", r)

View File

@ -18,16 +18,23 @@ func NewContext(store *sqlstore.Store) *Context {
} }
} }
func (auth *Context) ExpectUser(r *http.Request) *db.User { func (ctx *Context) ExpectUser(next func(*db.User, http.ResponseWriter, *http.Request)) http.Handler {
session, _ := auth.store.Get(r, "lishwist_user") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := ctx.store.Get(r, "lishwist_user")
username, ok := session.Values["username"].(string) username, ok := session.Values["username"].(string)
if !ok { if !ok {
log.Fatalln("Failed to get username") log.Println("Failed to get username")
http.Error(w, "", http.StatusInternalServerError)
return
} }
user, err := db.GetUserByName(username) user, err := db.GetUserByName(username)
if err != nil { if err != nil {
log.Fatalf("Failed to get user: %s\n", err) log.Printf("Failed to get user: %s\n", err)
http.Error(w, "", http.StatusInternalServerError)
return
} }
return user
next(user, w, r)
})
} }

View File

@ -2,11 +2,13 @@ package routing
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"strings" "strings"
) )
func writeGeneralError(w http.ResponseWriter, msg string, status int) { func writeGeneralError(w http.ResponseWriter, msg string, status int) {
log.Printf("General error: %s\n", msg)
w.WriteHeader(status) w.WriteHeader(status)
escapedMsg := strings.ReplaceAll(msg, `"`, `\"`) escapedMsg := strings.ReplaceAll(msg, `"`, `\"`)
_, _ = w.Write([]byte(fmt.Sprintf(`{"GeneralError":"%s"}`, escapedMsg))) _, _ = w.Write([]byte(fmt.Sprintf(`{"GeneralError":"%s"}`, escapedMsg)))

View File

@ -14,9 +14,8 @@ type foreignWishlistProps struct {
Gifts []db.Gift Gifts []db.Gift
} }
func (ctx *Context) ForeignWishlist(w http.ResponseWriter, r *http.Request) { func (ctx *Context) ForeignWishlist(user *db.User, w http.ResponseWriter, r *http.Request) {
userReference := r.PathValue("userReference") userReference := r.PathValue("userReference")
user := ctx.ExpectUser(r)
if user.Reference == userReference { if user.Reference == userReference {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return

View File

@ -14,8 +14,7 @@ type GroupProps struct {
CurrentUsername string CurrentUsername string
} }
func (ctx *Context) GroupPage(w http.ResponseWriter, r *http.Request) { func (ctx *Context) GroupPage(user *db.User, w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
groupReference := r.PathValue("groupReference") groupReference := r.PathValue("groupReference")
group, err := user.GetGroupByReference(groupReference) group, err := user.GetGroupByReference(groupReference)
if err != nil { if err != nil {

View File

@ -18,8 +18,7 @@ type HomeProps struct {
Groups []db.Group Groups []db.Group
} }
func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) { func (ctx *Context) Home(user *db.User, w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
gifts, err := user.GetGifts() gifts, err := user.GetGifts()
if err != nil { if err != nil {
error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err) error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err)
@ -39,20 +38,20 @@ func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) {
templates.Execute(w, "home.gotmpl", p) templates.Execute(w, "home.gotmpl", p)
} }
func (ctx *Context) HomePost(w http.ResponseWriter, r *http.Request) { func (ctx *Context) HomePost(user *db.User, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, "Couldn't parse form", http.StatusBadRequest) http.Error(w, "Couldn't parse form", http.StatusBadRequest)
return return
} }
switch r.Form.Get("intent") { switch r.Form.Get("intent") {
case "add_idea": case "add_idea":
ctx.WishlistAdd(w, r) ctx.WishlistAdd(user, w, r)
return return
case "delete_idea": case "delete_idea":
ctx.WishlistDelete(w, r) ctx.WishlistDelete(user, w, r)
return return
default: default:
ctx.TodoUpdate(w, r) ctx.TodoUpdate(user, w, r)
return return
} }
} }

View File

@ -6,7 +6,6 @@ import (
"lishwist/templates" "lishwist/templates"
"log" "log"
"net/http" "net/http"
"time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -79,8 +78,14 @@ func (ctx *Context) LoginPost(w http.ResponseWriter, r *http.Request) {
props.Username.Value = username props.Username.Value = username
user, err := db.GetUserByName(username) user, err := db.GetUserByName(username)
if user == nil || err != nil { if err != nil {
time.Sleep(time.Second) log.Printf("Failed to fetch user: %s\n", err)
props.GeneralError = "Username or password invalid"
ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
return
}
if user == nil {
log.Printf("User not found by name: %q\n", username)
props.GeneralError = "Username or password invalid" props.GeneralError = "Username or password invalid"
ctx.RedirectWithFlash(w, r, "/", "login_props", &props) ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
return return
@ -88,6 +93,7 @@ func (ctx *Context) LoginPost(w http.ResponseWriter, r *http.Request) {
passHash, err := user.GetPassHash() passHash, err := user.GetPassHash()
if err != nil { if err != nil {
log.Println("Failed to get password hash: " + err.Error())
props.GeneralError = "Something went wrong. Error code: Momo" props.GeneralError = "Something went wrong. Error code: Momo"
ctx.RedirectWithFlash(w, r, "/", "login_props", &props) ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
return return
@ -95,6 +101,7 @@ func (ctx *Context) LoginPost(w http.ResponseWriter, r *http.Request) {
err = bcrypt.CompareHashAndPassword(passHash, []byte(password)) err = bcrypt.CompareHashAndPassword(passHash, []byte(password))
if err != nil { if err != nil {
log.Println("Username or password invalid: " + err.Error())
props.GeneralError = "Username or password invalid" props.GeneralError = "Username or password invalid"
ctx.RedirectWithFlash(w, r, "/", "login_props", &props) ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
return return

View File

@ -0,0 +1,11 @@
package routing
import (
"net/http"
)
func NotFoundJson(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"GeneralError":"Not Found"}`))
w.Header().Add("Content-Type", "application/json")
}

View File

@ -13,6 +13,7 @@ func (ctx *Context) Register(w http.ResponseWriter, r *http.Request) {
session, _ := ctx.store.Get(r, "lishwist_user") session, _ := ctx.store.Get(r, "lishwist_user")
if flashes := session.Flashes("register_props"); len(flashes) > 0 { if flashes := session.Flashes("register_props"); len(flashes) > 0 {
log.Printf("Register found flashes: %#v\n", flashes)
flashProps, _ := flashes[0].(*api.RegisterProps) flashProps, _ := flashes[0].(*api.RegisterProps)
props.Username.Value = flashProps.Username.Value props.Username.Value = flashProps.Username.Value

View File

@ -1,12 +1,12 @@
package routing package routing
import ( import (
"lishwist/db"
"log" "log"
"net/http" "net/http"
) )
func (ctx *Context) TodoUpdate(w http.ResponseWriter, r *http.Request) { func (ctx *Context) TodoUpdate(user *db.User, w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return

22
server/routing/users.go Normal file
View File

@ -0,0 +1,22 @@
package routing
import (
"encoding/json"
"lishwist/db"
"net/http"
)
func (ctx *Context) UsersJson(user *db.User, w http.ResponseWriter, r *http.Request) {
if !user.IsAdmin {
NotFoundJson(w, r)
return
}
users, err := db.GetAllUsers()
if err != nil {
writeGeneralError(w, "Failed to get users: "+err.Error(), http.StatusBadRequest)
return
}
_ = json.NewEncoder(w).Encode(users)
}

View File

@ -1,12 +1,12 @@
package routing package routing
import ( import (
"lishwist/db"
"lishwist/error" "lishwist/error"
"net/http" "net/http"
) )
func (ctx *Context) WishlistAdd(w http.ResponseWriter, r *http.Request) { func (ctx *Context) WishlistAdd(user *db.User, w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@ -20,8 +20,7 @@ func (ctx *Context) WishlistAdd(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) { func (ctx *Context) WishlistDelete(user *db.User, w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@ -35,8 +34,7 @@ func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request) { func (ctx *Context) ForeignWishlistPost(user *db.User, w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
error.Page(w, "Failed to parse form...", http.StatusBadRequest, err) error.Page(w, "Failed to parse form...", http.StatusBadRequest, err)
return return