Compare commits

...

3 Commits

Author SHA1 Message Date
330d850790 Implementing api handlers
Added some groups handlers and how we obtain the user for the handler

Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-10 01:33:21 -04:00
5e6d061330 Adding/changing function declarations for Users storage interface
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-10 01:32:18 -04:00
91cfa901ca Adding function comment for lcrypto hashing
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-10 01:31:28 -04:00
8 changed files with 206 additions and 26 deletions

View File

@ -57,6 +57,9 @@ func (app *application) mount() http.Handler {
r.Use(middleware.CleanPath)
r.Use(middleware.Recoverer)
/// CSRF and SESSION TOKEN STUFF:
// Store session token in https or whatever only so js can't access it, and the store csrf locally using react or whatever, remove the cookie, and then include it in the http header under 'X-CSRF-Token' or as a hidden input in an html form
r.Use(middleware.Throttle(100)) // temporary or removable. throttles the whole thing to 1000 concurrent requests
// r.Use(cors.Handler(cors.Options{
// AllowedOrigins: []string{env.GetString("CORS_ALLOWED_ORIGIN", "http://localhost:5174")},
@ -96,7 +99,7 @@ func (app *application) mount() http.Handler {
r.Route("/user", func(r chi.Router) {
r.Route("/{userID}", func(r chi.Router) {
r.Use(app.AuthSessionMiddleware, app.CSRFCheckMiddleware, app.CheckUserMatchingMiddleware)
r.Use(app.AuthSessionMiddleware, app.CSRFCheckMiddleware, app.CheckUserMatchingMiddleware) // consider the actual use of the user matching middleware and whether an admin should be used instead
r.Get("/", app.getUserHandler)
@ -104,16 +107,20 @@ func (app *application) mount() http.Handler {
r.Get("/", app.getUsersGroupsHandler)
r.Route("/{groupID}", func(r chi.Router) {
r.Get("/", app.getUsersGroupHandler)
r.Delete("/", app.removeUserGroupHandler) // maybe this should expect authentication headers to reverify the password when deleting a group you own.
r.Put("/moderator", app.addGroupModeratorHandler)
r.Put("/owner", app.setGroupOwnerHandler)
r.Get("/owner", app.getGroupOwnerHandler)
r.Post("/moderator", app.addGroupModeratorHandler)
r.Delete("/moderator/{secondaryuserID}", app.removeModeratorPriviligesHandler)
r.Get("/users", app.getGroupUsersHandler)
r.Delete("/users/{secondaryuserID}", app.removeUserFromGroupHandler)
r.Post("/users", app.addGroupUserHandler) // needs to create a new user/do the whole invite thing if they don't already exist
r.Delete("/users/{secondaryuserID}", app.removeUserFromGroupHandler) // needs to check if the user is an owner/moderator
r.Put("/owner", app.setGroupOwnerHandler)
})
})
@ -130,6 +137,7 @@ func (app *application) mount() http.Handler {
r.Get("/", app.getReceiptImageHandler)
r.Put("/", app.changeReceiptImageHandler)
r.Delete("/", app.deleteReceiptImageHandler)
r.Post("/confirm", app.confirmImageCreationHandler)
})
})
})
@ -140,7 +148,7 @@ func (app *application) mount() http.Handler {
})
r.Use(app.CSRFCheckMiddleware)
// r.Use(app.CSRFCheckMiddleware)
r.Group(func(r chi.Router) {
r.Use(app.AuthSessionMiddleware)

View File

@ -89,3 +89,5 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque
func (app *application) refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
}
// func (app *application)

106
backend/cmd/api/groups.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"context"
"net/http"
"strconv"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/go-chi/chi/v5"
)
func (app *application) getUserGroups(ctx context.Context, userID int64) (*storage.UserGroups, error) {
if !app.config.redisCfg.enabled {
return app.store.Groups.GetUserGroups(ctx, userID)
}
usergroups, err := app.cacheStorage.UserGroups.Get(ctx, userID)
if err != nil {
return nil, err
}
if usergroups == nil {
usergroups, err = app.store.Groups.GetUserGroups(ctx, userID)
if err != nil {
return nil, err
}
if err := app.cacheStorage.UserGroups.Set(ctx, usergroups); err != nil {
return nil, err
}
}
return usergroups, nil
}
func (app *application) getGroup(ctx context.Context, groupID int64) (*storage.Group, error) {
if !app.config.redisCfg.enabled {
return app.store.Groups.GetByID(ctx, groupID)
}
group, err := app.cacheStorage.Groups.Get(ctx, groupID)
if err != nil {
return nil, err
}
if group == nil {
group, err = app.store.Groups.GetByID(ctx, groupID)
if err != nil {
return nil, err
}
if err := app.cacheStorage.Groups.Set(ctx, group); err != nil {
return nil, err
}
}
return group, nil
}
func (app *application) getUsersGroupsHandler(w http.ResponseWriter, r *http.Request) {
user, err := app.getUserFromURL(w, r)
if err != nil { // assume that the writing was already handled
return
}
usergroups, err := app.getUserGroups(r.Context(), user.ID)
if err != nil {
switch err {
case storage.ErrNotFound:
app.notFoundResponse(w, r, err)
return
default:
app.internalServerError(w, r, err)
return
}
}
if err := app.jsonResponse(w, http.StatusOK, usergroups); err != nil {
app.internalServerError(w, r, err)
}
}
func (app *application) getUsersGroupHandler(w http.ResponseWriter, r *http.Request) {
groupID, err := strconv.ParseInt(chi.URLParam(r, "groupID"), 10, 64)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
group, err := app.getGroup(r.Context(), groupID)
if err != nil {
switch err {
case storage.ErrNotFound:
app.notFoundResponse(w, r, err)
return
default:
app.internalServerError(w, r, err)
return
}
}
if err := app.jsonResponse(w, http.StatusOK, group); err != nil {
app.internalServerError(w, r, err)
}
}

View File

@ -5,6 +5,8 @@ import (
"net/http"
)
// copied from https://github.com/sikozonpc/GopherSocial/blob/main/cmd/api/json.go
func writeJSON(w http.ResponseWriter, status int, data any) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
@ -18,3 +20,21 @@ func writeJSONError(w http.ResponseWriter, status int, message string) error {
return writeJSON(w, status, &envelope{Error: message})
}
func readJSON(w http.ResponseWriter, r *http.Request, data any) error {
maxBytes := 1_048_578 // 1mb
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
return decoder.Decode(data)
}
func (app *application) jsonResponse(w http.ResponseWriter, status int, data any) error {
type envelope struct {
Data any `json:"data"`
}
return writeJSON(w, status, &envelope{Data: data})
}

View File

@ -3,16 +3,47 @@ package main
import (
"context"
"net/http"
"strconv"
auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/go-chi/chi/v5"
)
type userKey string
const userCtx userKey = "user"
func (app *application) getUserFromURL(w http.ResponseWriter, r *http.Request) (*storage.User, error) {
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
if err != nil {
app.badRequestResponse(w, r, err)
return nil, err
}
user, err := app.getUser(r.Context(), userID)
if err != nil {
switch err {
case storage.ErrNotFound:
app.notFoundResponse(w, r, err)
return nil, err
default:
app.internalServerError(w, r, err)
return nil, err
}
}
return user, nil
}
func (app *application) getUserHandler(w http.ResponseWriter, r *http.Request) {
user, err := app.getUserFromURL(w, r)
if err != nil { // assume that the writing was already handled
return
}
if err := app.jsonResponse(w, http.StatusOK, user); err != nil {
app.internalServerError(w, r, err)
}
}
func (app *application) getUsersHandler(w http.ResponseWriter, r *http.Request) {
@ -20,7 +51,7 @@ func (app *application) getUsersHandler(w http.ResponseWriter, r *http.Request)
func (app *application) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) getUser(ctx context.Context, userID int64) (*auth_storage.User, error) {
func (app *application) getUser(ctx context.Context, userID int64) (*storage.User, error) {
if !app.config.redisCfg.enabled {
return app.auth.Users.GetByID(ctx, userID)
}
@ -44,7 +75,7 @@ func (app *application) getUser(ctx context.Context, userID int64) (*auth_storag
return user, nil
}
func getUserFromContext(r *http.Request) *auth_storage.User {
user, _ := r.Context().Value(userCtx).(*auth_storage.User)
func getUserFromContext(r *http.Request) *storage.User {
user, _ := r.Context().Value(userCtx).(*storage.User)
return user
}

View File

@ -73,6 +73,7 @@ func (h *Hasher) HashStringWithSalt(in string, salt []byte) (string, error) {
return h.Algo.EncodeHash(hash)
}
// Generates a salt and hashes the given string with said salt
func (h *Hasher) Hash(in string) ([]byte, error) {
salt, err := GenerateRandomBytes(h.SaltLength)
if err != nil {

View File

@ -181,12 +181,17 @@ func (s *SQLUsersStore) Create(ctx context.Context, user *User) error { // creat
})
}
func (s *SQLUsersStore) CreateAndInvite(ctx context.Context, user *User, token string, exp time.Duration) error { // figure this out
func (s *SQLUsersStore) CreateAndInvite(ctx context.Context, user *User, token string, exp time.Duration) error {
// Implement the logic to create a user and invite them using the provided token with an expiration date.
return nil
}
func (s *SQLUsersStore) Activate(context.Context, string) error { // what does this do?
return nil
func (s *SQLUsersStore) Invite(ctx context.Context, exp time.Duration) (int64, string, error) {
return -1, "", nil
}
func (s *SQLUsersStore) CreateFromInvite(ctx context.Context, user *User) error {
// Implement the logic to create a user from an invite, possibly accepting terms of service or other conditions.
}
func (s *SQLUsersStore) delete(ctx context.Context, id int64, tx *sql.Tx) error {
@ -218,14 +223,13 @@ func (s *SQLUsersStore) Delete(ctx context.Context, id int64) error {
})
}
func (s *SQLUsersStore) UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error {
return nil
}
// func (s *SQLUsersStore) UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error {
// // Implement the logic to update a user's password if the old password matches and is correct.
// return nil
// }
func (s *SQLUsersStore) CheckPass(ctx context.Context, name string, pass string) (bool, error) {
return false, nil
}
func (s *SQLUsersStore) SigninUser(ctx context.Context, name string, pass string) (bool, *User, error) {
// CheckPass(ctx context.Context, name string, pass string) (bool, error)
func (s *SQLUsersStore) VerifyUserCredentials(ctx context.Context, username string, pass string) (bool, *User, error) {
// Implement the logic to verify user credentials and return a boolean indicating success or failure along with the User details if successful.
return false, nil, nil
}

View File

@ -19,16 +19,24 @@ type Storage struct {
GetByID(ctx context.Context, id int64) (*User, error)
GetByEmail(context.Context, string) (*User, error)
GetByUsername(context.Context, string) (*User, error)
Create(context.Context, *User) error // create a non-exported create function which does take in the tx
CreateAndInvite(ctx context.Context, user *User, token string, exp time.Duration) error // figure this out
Activate(context.Context, string) error // what does this do?
Create(context.Context, *User) error // create a non-exported create function which does take in the tx
CreateAndInvite(ctx context.Context, user *User, token string, exp time.Duration) error
Invite(ctx context.Context, exp time.Duration) (int64, string, error)
CreateFromInvite(ctx context.Context, user *User) error
Activate(context.Context, string) error
Delete(ctx context.Context, id int64) error
UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error
// UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error
// Look into resetting user passwords later
// CheckPass(ctx context.Context, name string, pass string) (bool, error)
// SigninUser(ctx context.Context, name string, pass string) (bool, *User, error)
VerifyUserCredentials(ctx context.Context, username string, pass string) (bool, *User, error)
// ValidCredentials(ctx context.Context, user *User, pass string) (bool, error)
// Figure out how to do user invites (so user account isn't truly made, no email, username, or password) and also how to do account creation with activation
// have createandinvite to do the activation stuff. so createandinvite -> activate does that process
// use createandinvite to create a blank user have a createFromInvite which takes a *User and adds everything but the id to the database. this should then send an activation link and do the activation. so a two stepper
}
Sessions interface { // store just session tokens, and their corresponding user id
@ -75,6 +83,6 @@ type Storage struct {
func NewSQLRedisMinIOStorage(db *sql.DB) Storage {
return Storage{
Users: &SQLUsersStore{db},
// Users: &SQLUsersStore{db},
}
}