package user
import (
"context"
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"golang.org/x/crypto/argon2"
"git.awl.red/~neallred/recipes/htmx"
"git.awl.red/~neallred/recipes/htp"
"git.awl.red/~neallred/recipes/logging"
"git.awl.red/~neallred/recipes/model"
)
type authKeyType struct{}
var authKey = authKeyType{}
func AuthFromContext(ctx context.Context) (*UserAuth, error) {
val := ctx.Value(authKey)
auth, ok := val.(*UserAuth)
if !ok {
return nil, fmt.Errorf("Could not get auth from context")
}
if auth == nil {
return nil, fmt.Errorf("Could not get valid auth from context")
}
return auth, nil
}
func OptionalAuthFromContext(ctx context.Context) *UserAuth {
val := ctx.Value(authKey)
auth, ok := val.(*UserAuth)
if !ok {
return nil
}
if auth == nil {
return nil
}
return auth
}
func AuthToReq(r *http.Request, auth *UserAuth) *http.Request {
return r.WithContext(context.WithValue(r.Context(), authKey, auth))
}
type AddRoleRequest struct {
Role *model.Role `json:"role"`
UserId *int64 `json:"userId"`
}
type SignupType int
const (
InvalidSignup SignupType = 0
UserPass SignupType = 1
)
type RestApi struct {
db *sql.DB
}
func NewRestApi(db *sql.DB) RestApi {
return RestApi{
db: db,
}
}
type HtmlApi struct {
db *sql.DB
}
func NewHtmlApi(db *sql.DB) HtmlApi {
return HtmlApi{
db: db,
}
}
func (a HtmlApi) GetSignup(w http.ResponseWriter, r *http.Request) {
optionalAuth := OptionalAuthFromContext(r.Context())
if optionalAuth != nil {
http.Redirect(w, r, "/recipes", http.StatusTemporaryRedirect)
return
}
props := (&htmx.FullPageProps{}).Nav(optionalAuth).Title("Signup")
htmx.Tmpl["signup.html"].ExecuteTemplate(w, "base", props)
}
func (a HtmlApi) GetLogin(w http.ResponseWriter, r *http.Request) {
optionalAuth := OptionalAuthFromContext(r.Context())
if optionalAuth != nil {
http.Redirect(w, r, "/recipes", http.StatusTemporaryRedirect)
return
}
props := (&htmx.FullPageProps{}).Nav(optionalAuth).Title("Login")
htmx.Tmpl["login.html"].ExecuteTemplate(w, "base", props)
}
func (a HtmlApi) PostSignup(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
err := r.ParseForm()
if err != nil {
htp.W400(w, log, err, "ventriloquial", htp.IssUsrNoUser)
return
}
user := r.PostForm.Get("username")
pass := r.PostForm.Get("password")
if user == "" {
htp.W400(w, log, nil, "clinquant", htp.IssUsrNoUser)
return
}
if pass == "" {
htp.W400(w, log, nil, "lathed", htp.IssUsrNoPass)
return
}
optionalAuth := OptionalAuthFromContext(r.Context())
if optionalAuth != nil {
w.Header().Set("HX-Redirect", "/recipes")
w.WriteHeader(http.StatusOK)
return
}
sessionToken, expires, err := a.Signup(user, pass)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
writeSession(w, sessionToken, expires)
w.Header().Set("HX-Redirect", "/recipes")
w.WriteHeader(http.StatusOK)
}
func (a HtmlApi) Signup(user, pass string) (string, time.Time, error) {
now := time.Now()
salt := getToken(256)
hashed := pwToHash(pass, salt)
query := "INSERT INTO user (username,password,salt) VALUES (?,?,?) RETURNING id"
var userId int64
tx, err := a.db.BeginTx(context.Background(), nil)
if err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "nullism", htp.IssUsrCreate, err.Error())
}
defer tx.Rollback()
newIdRow := tx.QueryRow(query, user, hashed, salt)
if err := newIdRow.Err(); err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "improvisatorial", htp.IssUsrCreate, err.Error())
}
err = newIdRow.Scan(&userId)
if err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "animalities", htp.IssUsrCreate, err.Error())
}
var addRolesQuery strings.Builder
addRolesQuery.WriteString(`INSERT INTO user_role
(user_id,role_id) VALUES
(?, (SELECT id FROM role WHERE role='ANONYMOUS'))
`)
userRoleArgs := []any{userId}
existingUsersRow := tx.QueryRow("SELECT count(*) from user;")
var userCount int
err = existingUsersRow.Scan(&userCount)
if err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "reinduct", htp.IssUsrCreate, err.Error())
}
if userCount == 0 {
return "", now, fmt.Errorf("%s: %s: %s", "vassalling", htp.BugAdminSignup, err.Error())
}
if userCount == 1 {
addRolesQuery.WriteString(`,
(?, (SELECT id FROM role WHERE role='USER')),
(?, (SELECT id FROM role WHERE role='ADMIN'));`)
userRoleArgs = append(userRoleArgs, userId, userId)
} else {
addRolesQuery.WriteRune(';')
}
_, err = tx.Exec(addRolesQuery.String(), userRoleArgs...)
if err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "orthophyre", htp.IssUsrCreate, err.Error())
}
addUserGroupArgs := []any{userId}
var addUserGroupQuery strings.Builder
addUserGroupQuery.WriteString(`INSERT INTO user_rgroup (user_id, rgroup_id) VALUES (?,(SELECT id FROM rgroup WHERE name='public'))`)
if userCount == 1 {
addUserGroupQuery.WriteString(`, (?,(SELECT id FROM rgroup WHERE name='logged_in'))`)
addUserGroupArgs = append(addUserGroupArgs, userId)
}
addUserGroupQuery.WriteRune(';')
_, err = tx.Exec(addUserGroupQuery.String(), addUserGroupArgs...)
if err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "embroilments", htp.IssUsrCreate, err.Error())
}
sessionToken, expires, err := createSession(tx, userId)
if err != nil {
return "", now, fmt.Errorf("%s: %s: %s", "upstrive", htp.IssUsrCreate, err.Error())
}
if tx.Commit() != nil {
return "", now, fmt.Errorf("%s: %s: %s", "arris", htp.IssUsrCreate, err.Error())
}
return sessionToken, expires, nil
}
func (api HtmlApi) Logout(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
authCookie, err := r.Cookie("auth")
var zeroTime time.Time
if authCookie == nil || err == http.ErrNoCookie {
writeSession(w, "", zeroTime)
if r.Method == http.MethodGet {
http.Redirect(w, r, "/recipes", http.StatusTemporaryRedirect)
} else {
w.Header().Set("HX-Redirect", "/recipes")
w.WriteHeader(http.StatusOK)
}
return
}
if err != nil {
htp.W500(w, log, err, "unnicknamed", htp.IssUsrLogoutSessionFind)
return
}
_, err = api.db.Exec("DELETE FROM session WHERE token = ?;", authCookie.Value)
if err != nil {
htp.W500(w, log, err, "coadaptation", htp.IssUsrSessionDel)
return
}
writeSession(w, "", zeroTime)
if r.Method == http.MethodGet {
http.Redirect(w, r, "/recipes", http.StatusTemporaryRedirect)
} else {
w.Header().Set("HX-Redirect", "/recipes")
w.WriteHeader(http.StatusOK)
}
return
}
func getToken(length int) string {
randomBytes := make([]byte, length)
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(randomBytes)[:length]
}
// interface usage allows passing *sql.DB or *sql.Tx
func createSession(db Execer, userId int64) (string, time.Time, error) {
// create a session
sessionToken := getToken(256)
expiry := time.Now().AddDate(0, 0, 14)
qSession := "INSERT INTO session (user_id,token,expires) VALUES (?,?,?);"
if _, err := db.Exec(qSession, userId, sessionToken, expiry); err != nil {
return "", time.Now(), err
}
return sessionToken, expiry, nil
}
func writeSession(w http.ResponseWriter, token string, expires time.Time) {
sessionCookie := http.Cookie{
HttpOnly: true,
Path: "/",
SameSite: http.SameSiteStrictMode,
Name: "auth",
Expires: expires,
Value: token,
}
http.SetCookie(w, &sessionCookie)
}
type Execer interface {
Exec(query string, args ...any) (sql.Result, error)
}
func pwToHash(pw string, salt string) string {
hashedPasswordBytes := argon2.IDKey([]byte(pw), []byte(salt), 1, 64*1024, 4, 256)
loginPasswordHash := base64.StdEncoding.EncodeToString(hashedPasswordBytes)
return loginPasswordHash
}
func (api HtmlApi) PostLogin(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
err := r.ParseForm()
if err != nil {
htp.W400(w, log, err, "ventriloquial", htp.IssUsrNoUser)
return
}
user := r.PostForm.Get("username")
pass := r.PostForm.Get("password")
if user == "" {
htp.W400(w, log, nil, "clinquant", htp.IssUsrNoUser)
return
}
if pass == "" {
htp.W400(w, log, nil, "lathed", htp.IssUsrNoPass)
return
}
qCheckUsers := "SELECT id,password,salt FROM user WHERE username=?;"
userRows, err := api.db.Query(qCheckUsers, user)
if err != nil {
htp.W500(w, log, err, "haemochrome", htp.IssUsrLoginFind)
return
}
var id int64
var dbPass string
var salt string
numUsers := 0
for userRows.Next() {
err := userRows.Scan(&id, &dbPass, &salt)
if err != nil {
htp.W500(w, log, err, "allocochick", htp.BugScan)
return
}
numUsers++
}
if numUsers != 1 {
if numUsers == 0 {
w.WriteHeader(http.StatusUnauthorized)
} else {
w.WriteHeader(http.StatusBadRequest)
}
log.Error("Bad number of users", "expected", 1, "actual", numUsers)
return
}
loginPasswordHash := pwToHash(pass, salt)
if dbPass != loginPasswordHash {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("403"))
log.Info("Password hash mismatch")
return
}
// create a session
sessionToken, expires, err := createSession(api.db, id)
if err != nil {
htp.W500(w, log, err, "prakrit", htp.IssUsrLoginSession)
return
}
writeSession(w, sessionToken, expires)
w.Header().Set("HX-Redirect", "/recipes")
w.WriteHeader(http.StatusOK)
}
type WhoamiToken struct {
Username string
UserId int64
Token string
Expires time.Time
}
type WhoamiResponse struct {
Username string `json:"username"`
UserId int64 `json:"userId"`
Roles []model.Role `json:"roles"`
LogoutTime time.Time `json:"logoutTime"`
}
type UserAuth struct {
Username string
UserId int64
Roles []model.Role
Expires time.Time
Priveleges map[model.Role]bool `json:"-"`
Groups map[int64]string `json:"-"`
}
func (ua UserAuth) Can(role model.Role) bool {
if ua.Priveleges == nil {
return false
}
can, _ := ua.Priveleges[role]
return can
}
func (ua *UserAuth) IsNil() bool {
return ua == nil
}
func (ua *UserAuth) IsUser() bool {
if ua.IsNil() {
return false
}
isUser, _ := ua.Priveleges[model.RoleUser]
return isUser
}
func (ua *UserAuth) IsAdmin() bool {
if ua.IsNil() {
return false
}
isUser, _ := ua.Priveleges[model.RoleAdmin]
return isUser
}
func (ua *UserAuth) IsAnonymous() bool {
if ua.IsNil() {
return true
}
isUser, _ := ua.Priveleges[model.RoleAnonymous]
return isUser
}
func findInList(list []model.Role, role model.Role) bool {
for _, v := range list {
if role == v {
return true
}
}
return false
}
func (ua *UserAuth) populatePrivileges() {
isAdmin := findInList(ua.Roles, model.RoleAdmin)
ua.Priveleges = map[model.Role]bool{
model.RoleAnonymous: true,
model.RoleUser: isAdmin || findInList(ua.Roles, model.RoleUser),
model.RoleAdmin: isAdmin,
}
}
func (ua *UserAuth) addGroups(groupIdsStr string, groupNames []string) error {
ua.Groups = map[int64]string{1: "public"}
if groupIdsStr == "" {
return nil
}
groupIdsStrs := strings.Split(groupIdsStr, ",")
for i, groupStr := range groupIdsStrs {
groupId, err := strconv.ParseInt(groupStr, 10, 64)
if err != nil {
return err
}
ua.Groups[groupId] = groupNames[i]
}
return nil
}
var (
ErrFailGetAuthCookie = fmt.Errorf("fail fetching auth cookie")
ErrNoAuthCookie = fmt.Errorf("no auth cookie")
errBadQuery = func(err error) error { return fmt.Errorf("error forming sql query: %s", err.Error()) }
errExpiredToken = fmt.Errorf("token expired, need to log in again")
)
const qToken = `SELECT
u.username,
s.user_id,
s.token,
s.expires,
group_concat(ug.rgroup_id),
group_concat(g.name)
FROM session s
JOIN user u ON s.user_id=u.id
JOIN user_rgroup ug ON ug.user_id=u.id
JOIN rgroup g ON ug.rgroup_id=g.id
WHERE token=?
GROUP BY s.id
LIMIT 1;`
func Auth(db *sql.DB, token string, setPrivelges bool) (*UserAuth, error) {
var whoamiToken WhoamiToken
row := db.QueryRow(qToken, token)
if err := row.Err(); err != nil {
return nil, fmt.Errorf("fail selecting whoami token %+v", err)
}
var groupIdsStr string
var groupNamesStr string
err := row.Scan(
&whoamiToken.Username,
&whoamiToken.UserId,
&whoamiToken.Token,
&whoamiToken.Expires,
&groupIdsStr,
&groupNamesStr,
)
if err != nil {
return nil, fmt.Errorf("fail selecting whoami token %+v", err)
}
if whoamiToken.Expires.Before(time.Now()) {
return nil, errExpiredToken
}
userId := whoamiToken.UserId
roles, err := getUserRoles(db, userId)
if err != nil {
return nil, fmt.Errorf("could not get roles for userId %d", userId)
}
userAuth := UserAuth{
Username: whoamiToken.Username,
UserId: userId,
Roles: roles,
Expires: whoamiToken.Expires,
}
userAuth.populatePrivileges()
userAuth.addGroups(groupIdsStr, strings.Split(groupNamesStr, ","))
return &userAuth, nil
}
func AuthFromHttp(w http.ResponseWriter, r *http.Request, db *sql.DB) (*UserAuth, error) {
authCookie, err := r.Cookie("auth")
if err != nil {
return nil, ErrFailGetAuthCookie
}
if authCookie == nil || authCookie.Value == "" {
return nil, ErrNoAuthCookie
}
return Auth(db, authCookie.Value, true)
}
func (api RestApi) Auth(w http.ResponseWriter, r *http.Request, needsPriveleges bool) (*UserAuth, error) {
authCookie, err := r.Cookie("auth")
if err != nil {
return nil, ErrFailGetAuthCookie
}
if authCookie == nil || authCookie.Value == "" {
return nil, ErrNoAuthCookie
}
return Auth(api.db, authCookie.Value, needsPriveleges)
}
const getUserRolesQuery = `SELECT r.role FROM role AS r
INNER JOIN user_role AS ur
ON ur.role_id = r.id AND ur.user_id = ?;`
func getUserRoles(db *sql.DB, userId int64) ([]model.Role, error) {
var roles []model.Role
rolesRows, err := db.Query(getUserRolesQuery, userId)
if err != nil {
return nil, err
}
for rolesRows.Next() {
var role model.Role
err := rolesRows.Scan(&role)
if err != nil {
return nil, err
}
roles = append(roles, role)
}
return roles, nil
}
var AuthCtxKey = &contextKey{"userAuth"}
type contextKey struct {
name string
}
func (api RestApi) RecipeGroups(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, err, "sough", htp.BugLogin)
return
}
htp.Json(w, log, "jural", userAuth.Groups)
}
func (api HtmlApi) UpdateUser(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "archnesses", htp.BugLogin)
return
}
err = r.ParseForm()
if err != nil {
htp.W400(w, log, err, "permutatorial", htp.IssUsrNoUser)
return
}
currentPassword := r.PostForm.Get("current_password")
newPassword := r.PostForm.Get("new_password")
confirmPassword := r.PostForm.Get("confirm_password")
if currentPassword == "" || newPassword == "" || confirmPassword == "" {
htp.W400(w, log, err, "ytterbia", "update user must send current_password, new_password, and confirm_password:")
return
}
if newPassword != confirmPassword {
htp.W400(w, log, err, "coralberry", "new password does not match confirm password")
return
}
var existingHash string
var salt string
existingRow := api.db.QueryRow("SELECT password,salt FROM user WHERE id=?;", userAuth.UserId)
err = existingRow.Scan(&existingHash, &salt)
if err != nil {
htp.W500(w, log, err, "palliobranchiate", "update user: bad read existing user password")
return
}
userInputHash := pwToHash(currentPassword, salt)
if userInputHash != existingHash {
htp.W400(w, log, err, "nannybush", "update user: passwords do not match")
return
}
newPwHash := pwToHash(newPassword, salt)
tx, err := api.db.Begin()
if err != nil {
htp.W500(w, log, err, "unsmirched", "change user: could not start transaction")
return
}
defer tx.Rollback()
_, err = tx.Exec("UPDATE user SET password=? WHERE id=?;", newPwHash, userAuth.UserId)
if err != nil {
htp.W500(w, log, err, "chandleress", "change user: could not update password")
return
}
_, err = tx.Exec("DELETE FROM session WHERE user_id=?;", userAuth.UserId)
if err != nil {
htp.W500(w, log, err, "postically", "change user: could not clear sessions")
return
}
sessionToken, expires, err := createSession(tx, userAuth.UserId)
if err != nil {
htp.W500(w, log, err, "isophone", "change user: could not add session")
return
}
tx.Commit()
writeSession(w, sessionToken, expires)
w.Header().Set("HX-Redirect", "/recipes")
w.WriteHeader(http.StatusOK)
}
func (api HtmlApi) GetUserEdit(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "archnesses", htp.BugLogin)
return
}
props := htmx.FullPageProps{}
props.Title("Edit user").Nav(userAuth)
htmx.Write(w, r, "user_edit.html", props)
}
func (api RestApi) AddRole(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
var addRoleRequest AddRoleRequest
if !htp.Read(w, r, log, "convexly", &addRoleRequest) {
return
}
if addRoleRequest.UserId == nil || addRoleRequest.Role == nil {
htp.W500(w, log, fmt.Errorf("no user id or role in add request"), "cherishes", htp.BadRolAdd)
return
}
tx, err := api.db.Begin()
if err != nil {
htp.W500(w, log, err, "inocarpus", htp.IssRolAdd)
return
}
defer tx.Rollback()
_, err = tx.Exec(
"INSERT INTO user_role (user_id,role_id) VALUES (?,(SELECT id FROM role WHERE role = ?)) ON CONFLICT DO NOTHING;",
*addRoleRequest.UserId,
string(*addRoleRequest.Role),
)
if err != nil {
htp.W500(w, log, err, "pseudoreformed", htp.IssRolAdd)
return
}
if *addRoleRequest.Role == model.RoleUser {
_, err := tx.Exec(
"INSERT INTO user_rgroup (user_id,rgroup_id) VALUES (?,(SELECT id from rgroup WHERE name = 'logged_in')) ON CONFLICT DO NOTHING;",
*addRoleRequest.UserId,
)
if err != nil {
htp.W500(w, log, err, "influents", htp.IssRolAdd)
return
}
}
err = tx.Commit()
if err != nil {
htp.W500(w, log, err, "micher", htp.IssRolAdd)
return
}
w.WriteHeader(http.StatusOK)
}
type LaterRecipe struct {
Id int64 `json:"id"`
RecipeId int64 `json:"recipeId"`
Preview string `json:"preview"`
}
type LaterNotes struct {
Recipes []LaterRecipe `json:"recipes"`
Notes string `json:"notes"`
}
type LaterNotesHtml struct {
htmx.FullPageProps
Recipes []LaterRecipe
Notes string
}
func (api HtmlApi) AddLaterRecipe(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "markman", htp.BugLogin)
return
}
recipeId := r.PathValue("id")
if recipeId == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
recipeIdInt, err := strconv.ParseInt(recipeId, 10, 64)
if err != nil {
htp.W400(w, log, err, "semimedicinal", htp.IssLtrAdd)
return
}
if recipeIdInt < 0 || recipeIdInt > 1_000_000_000 {
htp.W400(w, log, err, "elusoriness", htp.IssId)
}
_, err = api.db.Exec(
fmt.Sprintf(`
WITH rec AS (SELECT title from recipe WHERE id = %s)
INSERT INTO user_recipe_for_later (preview, recipe_id, user_id) VALUES ((SELECT title from rec),?,?) ON CONFLICT DO NOTHING;`, recipeId),
recipeId,
userAuth.UserId,
)
if err != nil {
htp.W500(w, log, err, "resurrectionism", htp.IssLtrAdd)
return
}
w.WriteHeader(http.StatusOK)
}
func (api HtmlApi) DeleteLaterRecipe(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "stinkeroos", htp.BugLogin)
return
}
recipeId := r.PathValue("id")
if recipeId == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
recipeIdInt, err := strconv.ParseInt(recipeId, 10, 64)
if err != nil {
htp.W400(w, log, err, "hypertranscendent", htp.IssLtrAdd)
return
}
if recipeIdInt < 1 || recipeIdInt > 1_000_000_000 {
htp.W400(w, log, err, "dimerism", htp.IssId)
}
_, err = api.db.Exec("DELETE FROM user_recipe_for_later WHERE id = ? AND user_id = ?;", recipeIdInt, userAuth.UserId)
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "antischolastic", htp.IssLtrDel)
return
}
w.WriteHeader(http.StatusOK)
}
func (api HtmlApi) getLaterNotes(w http.ResponseWriter, r *http.Request) (any, bool) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "sinusoid", htp.BugLogin)
return nil, false
}
laterNotes := LaterNotesHtml{
Recipes: []LaterRecipe{},
}
laterNotesRow := api.db.QueryRow("SELECT notes FROM user_later_notes WHERE user_id=?;", userAuth.UserId)
err = laterNotesRow.Scan(&laterNotes.Notes)
if err != nil && err != sql.ErrNoRows {
htp.W500(w, log, err, "unpatristically", htp.BugScan)
return nil, false
}
laterRecipeRows, err := api.db.Query("SELECT id,recipe_id,preview FROM user_recipe_for_later WHERE user_id=?;", userAuth.UserId)
if err != nil {
htp.W500(w, log, err, "lionism", htp.BugLogin)
return nil, false
}
for laterRecipeRows.Next() {
laterRecipe := LaterRecipe{}
err := laterRecipeRows.Scan(&laterRecipe.Id, &laterRecipe.RecipeId, &laterRecipe.Preview)
if err != nil {
htp.W500(w, log, err, "laryngectomized", htp.BugScan)
return nil, false
}
laterNotes.Recipes = append(laterNotes.Recipes, laterRecipe)
}
laterNotes.Nav(userAuth).Title("Saved recipes")
return laterNotes, true
}
func (api HtmlApi) GetLaterNotes(w http.ResponseWriter, r *http.Request) {
laterNotes, ok := api.getLaterNotes(w, r)
if !ok {
return
}
htmx.Write(w, r, "recipes_for_later.html", laterNotes)
}
func (api HtmlApi) GetLaterNotesEdit(w http.ResponseWriter, r *http.Request) {
laterNotes, ok := api.getLaterNotes(w, r)
if !ok {
return
}
htmx.Write(w, r, "recipes_for_later_edit.html", laterNotes)
}
type NotesInput struct {
Notes string `json:"notes"`
}
func (api HtmlApi) SetLaterNotes(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
userAuth, err := AuthFromContext(r.Context())
if err != nil {
htp.W500(w, log, fmt.Errorf("%s: %s", htp.WhyNoContext, err), "subdentate", htp.BugLogin)
return
}
err = r.ParseForm()
if err != nil {
htp.W400(w, log, err, "permutatorial", htp.IssUsrNoUser)
return
}
notes := r.PostForm.Get("notes")
_, err = api.db.Exec(`INSERT INTO user_later_notes (user_id, notes) VALUES (?,?)
ON CONFLICT (user_id) DO UPDATE SET notes = ?;`, userAuth.UserId, notes, notes)
if err != nil && err != sql.ErrNoRows {
htp.W500(w, log, err, "nametape", "Could not update saved recipe notes")
return
}
w.Header().Set("HX-Redirect", "/recipes_for_later")
w.WriteHeader(http.StatusOK)
}
type UserRole struct {
Id int64 `json:"id"`
Username string `json:"username"`
Updated time.Time `json:"updated"`
Roles []model.Role `json:"roles"`
}
func (api RestApi) GetUserRoles(w http.ResponseWriter, r *http.Request) {
log := logging.FromReq(r)
rows, err := api.db.Query(`SELECT u.id,u.username,u.updated,group_concat(r.role) FROM user u
LEFT JOIN user_role ur ON ur.user_id = u.id
LEFT JOIN role r ON ur.role_id = r.id
GROUP BY u.id
;`)
if err != nil {
htp.W500(w, log, err, "naivete", "Could not load roles")
return
}
var userRoles []UserRole
for rows.Next() {
rw := UserRole{}
var rolesStr string
err := rows.Scan(&rw.Id, &rw.Username, &rw.Updated, &rolesStr)
if err != nil {
htp.W500(w, log, err, "unglory", htp.BugScan)
return
}
rw.Roles = []model.Role{}
for _, role := range strings.Split(rolesStr, ",") {
rw.Roles = append(rw.Roles, model.Role(role))
}
userRoles = append(userRoles, rw)
}
htp.Json(w, log, "silicyl", userRoles)
}