Compare commits

...

30 Commits

Author SHA1 Message Date
16d783b1c8 Updating query mechanism for receipts 2025-08-09 16:34:11 -04:00
7effe9daa4 New TODO updates 2025-07-23 23:54:42 -04:00
10da175694 More updates to TODO.md
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-07-17 23:15:29 -04:00
7e813fa3f8 Added initial Todo list/notes
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-07-15 21:40:13 -04:00
d1a267eb44 Slight update to DB scheme
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-06-23 13:16:40 -04:00
47ff895b26 Partial implementation of multi-receipt querys
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-06-23 13:15:28 -04:00
a22bbbf5ab Adding function signatures for API handlers
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-06-23 13:11:35 -04:00
9b9305db29 Updating config and application struct and modifying api paths
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-06-23 13:10:22 -04:00
b687b43b35 Implementing Middleware for adding query params and groupID to context
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-06-23 13:09:41 -04:00
18330e745f Adding context variables to the internal section
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-06-23 13:08:23 -04:00
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
6a949ae682 Minor changes to image cleanup function and calling
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 10:27:50 -04:00
780b646d68 Minor fixes and formatting
delete should be called on the existing transaction for users.
Added an empty line in roles

Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 10:25:36 -04:00
30b4292c4a Updating sql Image structure to allow expiration of an image
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 10:24:39 -04:00
c91c784338 Redis image (struct not actual image) caching implementation
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 00:22:08 -04:00
c34c3dfa51 Fixing naming scheme for redis caching files 2025-05-07 00:18:37 -04:00
4fd63574c2 Implementing redis caching for receipts
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 00:16:54 -04:00
d3f03f2cae Modifying naming and adding cache key generating function
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 00:11:39 -04:00
d8b2bd7226 Implementing group caching
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 00:11:03 -04:00
2b2fa217d6 Implementing usergroups caching
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 00:01:04 -04:00
064faeadca Adding UserGroups struct to handle the list of groups a user is in
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-07 00:00:30 -04:00
a99ef367c7 Implementing roles caching
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:53:09 -04:00
29bf0bec88 Centralized the cache expiration times
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:52:28 -04:00
3bf0d93af2 Rearranging receipts struct and db
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:51:45 -04:00
03fbe92c7f Adding expiration times and interface for cached items
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:51:01 -04:00
2f328ec8ee Changing users groups to just return the group id
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:47:19 -04:00
dbd31faa6c Implementation of groups SQL interface for storage
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:11:15 -04:00
dbd273f2d6 Updating user sql interface for the correct password format
It's just a string, not a struct

Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2025-05-06 23:09:48 -04:00
26 changed files with 1097 additions and 88 deletions

53
TODO.md Normal file
View File

@ -0,0 +1,53 @@
1. Figure out the different structures and objects. receipts. receipt images. groups, so on.
2. Complete the api design
3. figure out the inbetween logic. especially for stuff that could include a transaction
4. transactional stuff maybe should be brought up to a higher level and we can just allow a transaction type thing to be passed through them
5. consider ignoring nosql as being an option for the system for anything other than storing receipts data maybe
ReceiptImage -> metadata, link or whatever is needed for S3 stuff
Receipt -> Many Receipt Images, receipt data, receipt metadata
Want to accomodate a single user. they can then group receipts. groups can be personal but also organization? should be able to filter which receipts are shown by personal vs organization groups? maybe by owned groups?
a group can also have certain people who are allowed to add other people, but also adding receipts vs just viewing receipts vs only adding and seeing their own stuff? [for this, there can be roles which are strict and placed into the roles table but also stored in the groupMembership table]
try and implement sso? it could probably work...
organizations can have groups themselves. they can be set to own the groups. an organization should have one owner and then admins. admins can do everything but stuff integral to the organization like deleting it, changing the name, that stuff.
anyone can make an organization and add people as admins.
- a user does not have to be part of an organization.
- an organization must have an owner
- a group must have an owner
- a receipt must be part of a group (if anything, it's the default group for the user, which is hidden)
- a group can have admins
- an admin cannot delete the group. a "super admin" (an organization admin) can though. super admins are at the same level as owners
- an organization can have groups
- an organization itself has no ability to add receipts
- a user must add a group under an organization to begin adding receipts
- an organization is really just a way to have multiple people acting like owners of a group and they get added automatically.
- maybe an organization can also group groups? so like subsections or suborganizations? call them sections. sections can nest
- a section or organization directly can have groups.
- alternatively, we can work with the section nesting and say that an organization by default has a section covering the whole thing, kind of like a users default group.
- a receipt cannot be part of multiple groups at once
- make groups so that they can have subgroups (just make groups nestable)
- make each receipt/group have individual role based permissions for the user
Figure out the permissions stuff. ACL maybe? idk
for the permissions, maybe something like
have group names be part of group names?
each group has a role. then that group role gets allowed to access the other stuff depending on their permissions
so for example, a role name could be organization::group::group::role
no/limited special character group names
set up a database migration to initialize the roles? or figure out the base roles in code

View File

@ -14,33 +14,43 @@ import (
"github.com/go-chi/httprate"
// "git.ewellenr.ca/receipt_indexer/backend/internal/auth"
"git.ewellenr.ca/receipt_indexer/backend/internal/env"
"git.ewellenr.ca/receipt_indexer/backend/internal/logger"
"git.ewellenr.ca/receipt_indexer/backend/internal/ratelimiter"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage/cache"
)
type application struct {
// set up the configs stuff here
config config
auth auth_storage.AuthStorage
store storage.Storage
logger logger.Logger
cacheStorage cache.Storage
rateLimiter ratelimiter.Limiter
environment env.Environment
store storage.Storage // for user storage. also stores authentication stuff
logger logger.Logger // logging events/actions/issues
cacheStorage cache.Storage // for caching the storage
rateLimiter ratelimiter.Limiter // rate limiting users
environment env.Environment // accessing environment variables for the setup
}
type config struct {
addr string
storeCfg storeConfig
envCfg envConfig
apiUrl string
frontendUrl string
auth authConfig
mail mailConfig
cacheCfg redisConfig
rateLimiter ratelimiter.Config
redisCfg redisConfig
// holds the different stuff like rate limiter, store, authenticator
}
type storeConfig struct {
db dbConfig
authStore authConfig
}
type redisConfig struct {
addr string
pw string
@ -57,6 +67,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 +109,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,32 +117,50 @@ func (app *application) mount() http.Handler {
r.Get("/", app.getUsersGroupsHandler)
r.Route("/{groupID}", func(r chi.Router) {
r.Use(app.addGroupToContextMiddleware)
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("/owner", app.setGroupOwnerHandler)
r.Get("/owner", app.getGroupOwnerHandler)
r.Put("/moderator", app.addGroupModeratorHandler)
r.Delete("/moderator/{secondaryuserID}", app.removeModeratorPriviligesHandler)
r.Get("/users", app.getGroupUsersHandler)
r.Delete("/users/{secondaryuserID}", app.removeUserFromGroupHandler)
r.Route("/users", func(r chi.Router) {
r.Use(app.CheckGroupRoleMiddleware) // need to verify that they are an owner/moderator
r.Get("/", app.getGroupUsersHandler)
r.Post("/", app.addGroupUserHandler) // needs to create a new user/do the whole invite thing if they don't already exist
r.Delete("/{secondaryuserID}", app.removeUserFromGroupHandler) // needs to check if the user is an owner/moderator
})
r.Put("/owner", app.setGroupOwnerHandler)
})
})
r.Route("/receipts", func(r chi.Router) {
r.Get("/", app.getReceiptsHandler)
r.Use(app.addQueryParamsToContextMiddleware)
r.Get("/", app.getReceiptsHandler) // batch get. should allow pagination and potentially a filter of some sort in the request. Does not return images or anything like that. Just the basic data (id, amount, date, location/store)
r.Route("/{receiptID}", func(r chi.Router) {
r.Use(app.receiptContextMiddleware)
r.Get("/", app.getReceiptHandler)
r.Delete("/", app.deleteReceiptHandler)
r.Route("/images", func(r chi.Router) {
r.Get("/", app.getReceiptImagesHandler)
r.Put("/", app.addReceiptImageHandler)
r.Post("/", app.addReceiptImageHandler)
r.Route("/{imageID}", func(r chi.Router) {
r.Get("/", app.getReceiptImageHandler)
r.Put("/", app.changeReceiptImageHandler)
r.Delete("/", app.deleteReceiptImageHandler)
r.Put("/confirm", app.confirmImageCreationHandler)
})
})
})
@ -140,7 +171,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)

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

@ -0,0 +1,131 @@
package main
import (
"context"
"net/http"
"strconv"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/go-chi/chi/v5"
)
type groupKey string
const groupCtx groupKey = "group"
func (app *application) getUserGroups(ctx context.Context, userID int64) (*storage.UserGroups, error) {
if !app.config.cacheCfg.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.cacheCfg.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)
}
}
func (app *application) setGroupOwnerHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) getGroupOwnerHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) addGroupModeratorHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) removeModeratorPriviligesHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) getGroupUsersHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) addGroupUserHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) removeUserFromGroupHandler(w http.ResponseWriter, r *http.Request) {
}

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

@ -7,7 +7,8 @@ import (
"net/http"
"strconv"
auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
// auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
l_context "git.ewellenr.ca/receipt_indexer/backend/internal/context"
"github.com/go-chi/chi/v5"
)
@ -27,7 +28,7 @@ func (app *application) AuthSessionMiddleware(next http.Handler) http.Handler {
return
}
valid, userID, err := app.auth.Sessions.CheckSession(r.Context(), token) // should have a different function for this
valid, userID, err := app.store.Sessions.CheckSession(r.Context(), token) // should have a different function for this
if !valid {
app.unauthorizedErrorResponse(w, r, fmt.Errorf("Invalid session token"))
return
@ -131,7 +132,7 @@ func (app *application) RateLimiterMiddleware(next http.Handler) http.Handler {
})
}
func (app *application) receiptsContextMiddleware(next http.Handler) http.Handler {
func (app *application) receiptContextMiddleware(next http.Handler) http.Handler {
// add the receipt id to the context? or the receipt class to the context
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -163,7 +164,7 @@ func (app *application) receiptsContextMiddleware(next http.Handler) http.Handle
}
func (app *application) checkRolePrecedence(ctx context.Context, user *auth_storage.User, roleName string) (bool, error) {
role, err := app.auth.Roles.GetByName(ctx, roleName)
role, err := app.store.Roles.GetByName(ctx, roleName)
if err != nil {
return false, err
}
@ -195,3 +196,36 @@ func (app *application) checkReceiptOwnership(requiredRole string, next http.Han
next.ServeHTTP(w, r)
})
}
func (app *application) addGroupToContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlGroupID, err := strconv.ParseInt(chi.URLParam(r, "groupID"), 10, 64)
if err != nil {
app.badRequestResponse(w, r, fmt.Errorf("Invalid url group ID - Not an integer"))
return
}
ctx := r.Context()
group, err := app.getGroup(ctx, urlGroupID)
if err != nil {
app.unauthorizedErrorResponse(w, r, err)
return
}
ctx = context.WithValue(ctx, groupCtx, group)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (app *application) addQueryParamsToContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// need to select the user from the token validation stuff
ctx := r.Context()
ctx = context.WithValue(ctx, l_context.QueryParamsCtx, r.URL.Query())
// make sure to add user and role into the context here
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@ -4,21 +4,40 @@ import (
"context"
"net/http"
l_context "git.ewellenr.ca/receipt_indexer/backend/internal/context"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage/cache"
)
type receiptKey string
const receiptCtx receiptKey = "receipt"
type receiptsQuery struct {
Length int `json:"length"`
List []*storage.Receipt `json:"list"`
}
func (app *application) getReceiptsHandler(w http.ResponseWriter, r *http.Request) {
// get the page and size from context
// default them to something
receipts, err := app.getReceipts(r.Context(), getUserFromContext(r).ID)
if err != nil {
app.internalServerError(w, r, err)
return
}
if err := app.jsonResponse(w, http.StatusOK, &receiptsQuery{Length: len(receipts), List: receipts}); err != nil {
app.internalServerError(w, r, err)
}
}
func (app *application) getReceiptHandler(w http.ResponseWriter, r *http.Request) {
receipt, err := app.getReceipt(r.Context(), getReceiptFromContext(r).ID)
if err != nil {
app.internalServerError(w, r, err)
return
}
if err := app.jsonResponse(w, http.StatusOK, receipt); err != nil {
app.internalServerError(w, r, err)
}
}
func (app *application) createReceiptHandler(w http.ResponseWriter, r *http.Request) {
@ -41,7 +60,7 @@ func (app *application) deleteReceiptHandler(w http.ResponseWriter, r *http.Requ
}
func (app *application) getReceipt(ctx context.Context, receiptID int64) (*storage.Receipt, error) {
if !app.config.redisCfg.enabled {
if !app.config.cacheCfg.enabled {
return app.store.Receipts.GetByID(ctx, receiptID)
}
@ -64,7 +83,51 @@ func (app *application) getReceipt(ctx context.Context, receiptID int64) (*stora
return receipt, nil
}
func (app *application) getReceipts(ctx context.Context, userID int64) ([]*storage.Receipt, error) {
if !app.config.cacheCfg.enabled {
return app.store.Receipts.GetForUser(ctx, userID)
}
// encode the query into a string so that it's "indexable"
queryID, err := storage.EncodeReceiptQuery(ctx, userID)
receiptIDs, err := app.cacheStorage.ReceiptList.Get(ctx, queryID)
if err != nil {
return nil, err
}
var receipts []*storage.Receipt = nil
if receiptIDs == nil {
receipts, err := app.store.Receipts.GetForUser(ctx, userID)
if err != nil {
return nil, err
}
for _, val := range receipts {
receiptIDs = append(receiptIDs, val.ID)
}
if err := app.cacheStorage.ReceiptList.Set(ctx, cache.ReceiptListKeyVal{UserID: userID, QueryEncoding: queryID, List: receiptIDs}); err != nil {
return nil, err
}
} else {
for _, id := range receiptIDs {
temp_receipt, err := app.getReceipt(ctx, id)
if err != nil {
return nil, err
}
receipts = append(receipts, temp_receipt)
}
}
return receipts, nil
}
func getReceiptFromContext(r *http.Request) *storage.Receipt {
receipt, _ := r.Context().Value(receiptCtx).(*storage.Receipt)
receipt, _ := r.Context().Value(l_context.ReceiptCtx).(*storage.Receipt)
return receipt
}

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,9 +51,12 @@ 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) {
if !app.config.redisCfg.enabled {
return app.auth.Users.GetByID(ctx, userID)
func (app *application) removeUserGroupHandler(w http.ResponseWriter, r *http.Request) {
}
func (app *application) getUser(ctx context.Context, userID int64) (*storage.User, error) {
if !app.config.cacheCfg.enabled {
return app.store.Users.GetByID(ctx, userID)
}
user, err := app.cacheStorage.Users.Get(ctx, userID)
@ -31,7 +65,7 @@ func (app *application) getUser(ctx context.Context, userID int64) (*auth_storag
}
if user == nil {
user, err = app.auth.Users.GetByID(ctx, userID)
user, err = app.store.Users.GetByID(ctx, userID)
if err != nil {
return nil, err
}
@ -44,7 +78,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

@ -0,0 +1,23 @@
package context
import "net/http"
type (
pageKey string
pagesizeKey string
queryParamsKey string
receiptKey string
)
const (
PageCtx pageKey = "page"
PagesizeCtx pagesizeKey = "pageSize"
QueryParamsCtx queryParamsKey = "queryParams"
ReceiptCtx receiptKey = "receipt"
)
func getPageAndSizeFromContext(r *http.Request) (int, int) {
pageNumber, _ := r.Context().Value(PageCtx).(int)
pageSize, _ := r.Context().Value(PagesizeCtx).(int)
return pageNumber, pageSize
}

View File

@ -36,9 +36,9 @@ Table roles {
Table reciepts {
id integer [primary key]
groupid integer
data nvarchar
created_at timestamp
updated_at timestamp
data jsonb
}
// Table imageOwnership {
@ -52,7 +52,8 @@ Table images {
receiptid integer
created_at timestamp
path varchar
added bool
expiration timestamp
added bool [default: false]
}
@ -73,4 +74,4 @@ Ref: "roles"."id" < "users"."role"
Ref: "reciepts"."id" < "images"."receiptid"
Ref: "groups"."id" < "reciepts"."groupownerid"
Ref: "groups"."id" < "reciepts"."groupid"

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

@ -2,22 +2,53 @@ package cache
import (
"context"
"time"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
// auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
)
const (
UserExpTime = time.Minute
RoleExpTime = time.Minute
GroupExpTime = time.Minute
UserGroupsExpTime = time.Minute
ReceiptExpTime = time.Minute * 5
ReceiptsQueryExpTime = time.Minute
ReceiptImageExpTime = time.Minute
)
type Storage struct {
Users interface {
Get(ctx context.Context, id int64) (*storage.User, error)
Set(ctx context.Context, user *storage.User) error
Delete(ctx context.Context, userID int64)
}
Roles interface {
Get(ctx context.Context, name string) (*storage.Role, error)
Set(ctx context.Context, role *storage.Role) error
Delete(ctx context.Context, name string)
}
Groups interface {
Get(ctx context.Context, id int64) (*storage.Group, error)
Set(ctx context.Context, group *storage.Group) error
Delete(ctx context.Context, id int64)
}
UserGroups interface {
Get(ctx context.Context, userid int64) (*storage.UserGroups, error)
Set(ctx context.Context, groups *storage.UserGroups) error
Delete(ctx context.Context, userid int64)
}
Receipts interface {
Get(ctx context.Context, id int64) (*storage.Receipt, error)
Set(ctx context.Context, receipt *storage.Receipt) error
Delete(ctx context.Context, id int64)
}
ReceiptList interface {
Get(ctx context.Context, queryencoding string) ([]int64, error)
Set(ctx context.Context, receiptidList ReceiptListKeyVal) error
Delete(ctx context.Context, queryencoding string)
}
ReceiptImage interface {
Get(ctx context.Context, id int64) (*storage.Image, error)
Set(ctx context.Context, image *storage.Image) error

View File

@ -0,0 +1,55 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/redis/go-redis/v9"
)
type GroupStore struct {
rdb *redis.Client
}
func (s *GroupStore) generateCacheKey(id int64) string {
return fmt.Sprintf("group-%d", id)
}
func (s *GroupStore) Get(ctx context.Context, id int64) (*storage.Group, error) {
cacheKey := s.generateCacheKey(id)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var group storage.Group
if data != "" {
err := json.Unmarshal([]byte(data), &group)
if err != nil {
return nil, err
}
}
return &group, nil
}
func (s *GroupStore) Set(ctx context.Context, group *storage.Group) error {
cacheKey := s.generateCacheKey(group.ID)
json, err := json.Marshal(group)
if err != nil {
return err
}
return s.rdb.Set(ctx, cacheKey, json, RoleExpTime).Err()
}
func (s *GroupStore) Delete(ctx context.Context, id int64) {
cacheKey := s.generateCacheKey(id)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -0,0 +1,55 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/redis/go-redis/v9"
)
type ReceiptImageStore struct {
rdb *redis.Client
}
func (s *ReceiptImageStore) generateCacheKey(id int64) string {
return fmt.Sprintf("receiptimage-%d", id)
}
func (s *ReceiptImageStore) Get(ctx context.Context, id int64) (*storage.Image, error) {
cacheKey := s.generateCacheKey(id)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var receiptimage storage.Image
if data != "" {
err := json.Unmarshal([]byte(data), &receiptimage)
if err != nil {
return nil, err
}
}
return &receiptimage, nil
}
func (s *ReceiptImageStore) Set(ctx context.Context, receiptimage *storage.Image) error {
cacheKey := s.generateCacheKey(receiptimage.ID)
json, err := json.Marshal(receiptimage)
if err != nil {
return err
}
return s.rdb.Set(ctx, cacheKey, json, ReceiptImageExpTime).Err()
}
func (s *ReceiptImageStore) Delete(ctx context.Context, id int64) {
cacheKey := s.generateCacheKey(id)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -0,0 +1,55 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/redis/go-redis/v9"
)
type ReceiptStore struct {
rdb *redis.Client
}
func (s *ReceiptStore) generateCacheKey(id int64) string {
return fmt.Sprintf("receipt-%d", id)
}
func (s *ReceiptStore) Get(ctx context.Context, id int64) (*storage.Receipt, error) {
cacheKey := s.generateCacheKey(id)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var receipt storage.Receipt
if data != "" {
err := json.Unmarshal([]byte(data), &receipt)
if err != nil {
return nil, err
}
}
return &receipt, nil
}
func (s *ReceiptStore) Set(ctx context.Context, receipt *storage.Receipt) error {
cacheKey := s.generateCacheKey(receipt.ID)
json, err := json.Marshal(receipt)
if err != nil {
return err
}
return s.rdb.Set(ctx, cacheKey, json, ReceiptExpTime).Err()
}
func (s *ReceiptStore) Delete(ctx context.Context, id int64) {
cacheKey := s.generateCacheKey(id)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -0,0 +1,60 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
)
type ReceiptListKeyVal struct {
UserID int64 `json:"user_id"`
QueryEncoding string `json:"query_encoding"`
List []int64 `json:"list"`
}
type ReceiptListStore struct {
rdb *redis.Client
}
func (s *ReceiptListStore) generateCacheKey(encoding string) string {
return fmt.Sprintf("receiptList-%s", encoding)
}
func (s *ReceiptListStore) Get(ctx context.Context, queryencoding string) ([]int64, error) {
cacheKey := s.generateCacheKey(queryencoding)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var receipts ReceiptListKeyVal
if data != "" {
err := json.Unmarshal([]byte(data), &receipts)
if err != nil {
return nil, err
}
}
return receipts.List, nil
}
func (s *ReceiptListStore) Set(ctx context.Context, receiptidList ReceiptListKeyVal) error {
cacheKey := s.generateCacheKey(receiptidList.QueryEncoding)
json, err := json.Marshal(receiptidList)
if err != nil {
return err
}
return s.rdb.Set(ctx, cacheKey, json, ReceiptsQueryExpTime).Err()
}
func (s *ReceiptListStore) Delete(ctx context.Context, queryencoding string) {
cacheKey := s.generateCacheKey(queryencoding)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -0,0 +1,54 @@
package cache
import (
"context"
"encoding/json"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/redis/go-redis/v9"
)
type RoleStore struct {
rdb *redis.Client
}
func (s *RoleStore) generateCacheKey(name string) string {
return name
}
func (s *RoleStore) Get(ctx context.Context, name string) (*storage.Role, error) {
cacheKey := s.generateCacheKey(name)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var role storage.Role
if data != "" {
err := json.Unmarshal([]byte(data), &role)
if err != nil {
return nil, err
}
}
return &role, nil
}
func (s *RoleStore) Set(ctx context.Context, role *storage.Role) error {
cacheKey := s.generateCacheKey(role.Name)
json, err := json.Marshal(role)
if err != nil {
return err
}
return s.rdb.Set(ctx, cacheKey, json, RoleExpTime).Err()
}
func (s *RoleStore) Delete(ctx context.Context, name string) {
cacheKey := s.generateCacheKey(name)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -4,9 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"time"
// auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/redis/go-redis/v9"
)
@ -15,10 +13,12 @@ type UserStore struct {
rdb *redis.Client
}
const UserExpTime = time.Minute
func (s *UserStore) generateCacheKey(id int64) string {
return fmt.Sprintf("user-%d", id)
}
func (s *UserStore) Get(ctx context.Context, userID int64) (*storage.User, error) {
cacheKey := fmt.Sprintf("user-%d", userID)
cacheKey := s.generateCacheKey(userID)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
@ -39,7 +39,7 @@ func (s *UserStore) Get(ctx context.Context, userID int64) (*storage.User, error
}
func (s *UserStore) Set(ctx context.Context, user *storage.User) error {
cacheKey := fmt.Sprintf("user-%d", user.ID)
cacheKey := s.generateCacheKey(user.ID)
json, err := json.Marshal(user)
if err != nil {
@ -50,6 +50,6 @@ func (s *UserStore) Set(ctx context.Context, user *storage.User) error {
}
func (s *UserStore) Delete(ctx context.Context, userID int64) {
cacheKey := fmt.Sprintf("user-%d", userID)
cacheKey := s.generateCacheKey(userID)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -0,0 +1,55 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
"github.com/redis/go-redis/v9"
)
type UserGroupsStore struct {
rdb *redis.Client
}
func (s *UserGroupsStore) generateCacheKey(id int64) string {
return fmt.Sprintf("user-%d", id)
}
func (s *UserGroupsStore) Get(ctx context.Context, userid int64) (*storage.UserGroups, error) {
cacheKey := s.generateCacheKey(userid)
data, err := s.rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var usergroups storage.UserGroups
if data != "" {
err := json.Unmarshal([]byte(data), &usergroups)
if err != nil {
return nil, err
}
}
return &usergroups, nil
}
func (s *UserGroupsStore) Set(ctx context.Context, groups *storage.UserGroups) error {
cacheKey := s.generateCacheKey(groups.UserID)
json, err := json.Marshal(groups)
if err != nil {
return err
}
return s.rdb.Set(ctx, cacheKey, json, RoleExpTime).Err()
}
func (s *UserGroupsStore) Delete(ctx context.Context, userid int64) {
cacheKey := s.generateCacheKey(userid)
s.rdb.Del(ctx, cacheKey)
}

View File

@ -7,3 +7,8 @@ type Group struct {
Owner int64 `json:"owner"`
Moderators []int64 `json:"moderators"`
}
type UserGroups struct {
UserID int64 `json:"user_id"`
GroupIDs []int64 `json:"group_ids"`
}

View File

@ -1,15 +1,23 @@
package storage
import "time"
import (
"context"
"encoding/base64"
"encoding/json"
"net/url"
"strconv"
"time"
l_context "git.ewellenr.ca/receipt_indexer/backend/internal/context"
)
type Receipt struct {
ID int64 `json:"id"`
// Owner string `json:"username"`
ID int64 `json:"id"`
OwnerID int64 `json:"user_id"`
ImageIDs []int64 `json:"image_ids"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Data ReceiptData `json:"receipt_data"`
CreatedAt time.Time `json:"created_at`
UpdatedAt time.Time `json:"updated_at`
}
type ReceiptData struct {
@ -20,3 +28,123 @@ type ReceiptData struct {
Items map[string]float64 `json:"items"`
// Currency string `json:"currency"`
}
type ReceiptsQuery struct {
UserID int64 `json:"user_id"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Page int `json:"page"`
Size int `json:"size"`
SearchTerms []string `json:"search_terms"`
MinTotal int `json:"min_amount"`
MaxTotal int `json:"max_amount"`
}
var (
pageNumberKey = "page"
pageSizeKey = "size"
timeStartKey = "time_start"
timeEndKey = "time_end"
searchTermsKey = "search_terms"
totalMinKey = "total_min"
totalMaxKey = "total_max"
)
const stringifiedQueryMaxLength = 524288000 // 500MiB
func newReceiptQuery() ReceiptsQuery {
return ReceiptsQuery{
UserID: -1,
StartDate: time.Time{},
EndDate: time.Time{},
Page: 1,
Size: 20,
SearchTerms: nil,
MinTotal: -1,
MaxTotal: -1,
}
}
func EncodeReceiptQuery(ctx context.Context, userID int64) (string, error) {
receiptQuery := newReceiptQuery()
receiptQuery.UserID = userID
var urlParams url.Values = nil
if ctx.Value(l_context.QueryParamsCtx) != nil {
urlParams = ctx.Value(l_context.QueryParamsCtx).(url.Values)
}
if urlParams == nil {
return "", ErrInvalidReceiptsQuery
}
// Extract the search terms
if urlParams.Has(pageNumberKey) {
temp, err := strconv.Atoi(urlParams.Get(pageNumberKey))
if err != nil {
return "", err
}
receiptQuery.Page = temp
}
if urlParams.Has(pageSizeKey) {
temp, err := strconv.Atoi(urlParams.Get(pageSizeKey))
if err != nil {
return "", err
}
receiptQuery.Page = temp
}
if urlParams.Has(timeStartKey) {
temp, err := time.Parse(time.DateTime, urlParams.Get(timeStartKey))
if err != nil {
return "", err
}
receiptQuery.StartDate = temp
}
if urlParams.Has(timeEndKey) {
temp, err := time.Parse(time.DateTime, urlParams.Get(timeEndKey))
if err != nil {
return "", err
}
receiptQuery.EndDate = temp
}
if urlParams.Has(totalMinKey) {
temp, err := strconv.Atoi(urlParams.Get(totalMinKey))
if err != nil {
return "", err
}
receiptQuery.MinTotal = temp
}
if urlParams.Has(totalMaxKey) {
temp, err := strconv.Atoi(urlParams.Get(totalMaxKey))
if err != nil {
return "", err
}
receiptQuery.MaxTotal = temp
}
if urlParams.Has(searchTermsKey) {
temp := urlParams[searchTermsKey]
receiptQuery.SearchTerms = temp
}
// encoding the query into a string so that it can be used as a query term
jsonEncoding, err := json.Marshal(receiptQuery)
if err != nil {
return "", err
}
out := base64.StdEncoding.EncodeToString(jsonEncoding)
// convert userID to base64 encoding of the int (so bitwise of the int and then into base64)
if len(out) > stringifiedQueryMaxLength { // Makes the assumption that none of the other filters would cause the overflow of 500MiB
return "", ErrSearchTermTooLong
}
return out, nil
}

View File

@ -9,7 +9,7 @@ type SQLGroupsStore struct {
db *sql.DB
}
func (s *SQLGroupsStore) GetByID(ctx context.Context, id int64) (*Group, error) {
func (s *SQLGroupsStore) getByID(ctx context.Context, id int64) (*Group, error) {
query := `SELECT id, name, owner FROM groups WHERE id = $1`
group := &Group{}
@ -18,20 +18,100 @@ func (s *SQLGroupsStore) GetByID(ctx context.Context, id int64) (*Group, error)
return nil, err
}
query = `SELECT userid, moderator FROM groupMembership WHERE groupid = $1`
rows, err := s.db.QueryContext(ctx, query, id)
if err != nil {
return nil, err
}
for rows.Next() {
var userid int64
var moderator bool
err := rows.Scan(&userid, &moderator)
if err != nil {
return nil, err
}
if moderator {
group.Moderators = append(group.Moderators, userid)
} else {
group.Users = append(group.Users, userid)
}
}
return group, nil
}
func (s *SQLGroupsStore) GetUserGroups(ctx context.Context, userID int64) ([]*Group, error) {
// Implement logic to retrieve user's groups from the database
}
func (s *SQLGroupsStore) GetUsersInGroup(ctx context.Context, groupId int64) ([]*User, error) {
// Implement logic to retrieve users in a group from the database
func (s *SQLGroupsStore) GetByID(ctx context.Context, id int64) (*Group, error) {
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
return s.getByID(ctx, id)
}
func (s *SQLGroupsStore) GetUserGroups(ctx context.Context, userID int64) (*UserGroups, error) {
// Implement logic to retrieve user's groups from the database
usergroups := &UserGroups{
UserID: userID,
}
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
query := `SELECT groupid from groupMembership WHERE userid = $1`
rows, err := s.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, err
}
for rows.Next() {
var groupid int64
err := rows.Scan(&groupid)
if err != nil {
return nil, err
}
usergroups.GroupIDs = append(usergroups.GroupIDs, groupid)
}
return usergroups, nil
}
// func (s *SQLGroupsStore) GetUsersInGroup(ctx context.Context, groupId int64) ([]*User, error) {
// // Implement logic to retrieve users in a group from the database
// }
func (s *SQLGroupsStore) Create(ctx context.Context, group *Group) error {
// Implement logic to create a new group in the database
query := `INSERT INTO groups (name, owner) VALUES ($1, $2) RETURNING id`
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
err := s.db.QueryRowContext(ctx, query, group.Name, group.Owner).Scan(&group.ID)
if err != nil {
return err
}
return nil
}
func (s *SQLGroupsStore) Delete(ctx context.Context, id int64) error {
// Implement logic to delete a group from the database
query := `DELETE FROM groups WHERE id = $1`
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
res, err := s.db.ExecContext(ctx, query, id)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return ErrNotFound
}
return nil
}

View File

@ -3,17 +3,26 @@ package storage
import (
"context"
"database/sql"
"time"
)
type SQLImagesStore struct {
db *sql.DB
}
func (s SQLImagesStore) GetByID(ctx context.Context, id int64) (*Image, error) {
query := `SELECT id, receiptid, created_at, path FROM images WHERE id = $1`
const imgStoreCleanupTime = time.Hour * 12
func NewSQLImageStore(db *sql.DB) *SQLImagesStore {
imgstore := &SQLImagesStore{db}
go imgstore.cleanupDeadImages(context.Background(), imgStoreCleanupTime)
return imgstore
}
func (s *SQLImagesStore) GetByID(ctx context.Context, id int64) (*Image, error) {
query := `SELECT id, receiptid, created_at, path FROM images WHERE id = $1 AND added = $2`
image := &Image{}
err := s.db.QueryRowContext(ctx, query, id).Scan(&image.ID, &image.ReceiptID, &image.CreatedAt, &image.Path)
err := s.db.QueryRowContext(ctx, query, id, true).Scan(&image.ID, &image.ReceiptID, &image.CreatedAt, &image.Path)
if err != nil {
return nil, err
}
@ -21,10 +30,10 @@ func (s SQLImagesStore) GetByID(ctx context.Context, id int64) (*Image, error) {
return image, nil
}
func (s SQLImagesStore) Create(ctx context.Context, img *Image) error {
func (s *SQLImagesStore) Create(ctx context.Context, img *Image, exp time.Duration) error {
query := `
INSERT INTO images (receiptid, path)
VALUES ($1, $2) RETURNING id, created_at`
INSERT INTO images (receiptid, path, expiration)
VALUES ($1, $2, $3) RETURNING id, created_at`
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
@ -34,6 +43,7 @@ func (s SQLImagesStore) Create(ctx context.Context, img *Image) error {
query,
img.ReceiptID,
img.Path, // might need to marshal or serialize this
time.Now().Add(exp),
).Scan(
&img.ID,
&img.CreatedAt,
@ -41,7 +51,7 @@ func (s SQLImagesStore) Create(ctx context.Context, img *Image) error {
return err
}
func (s SQLImagesStore) Delete(ctx context.Context, id int64) error {
func (s *SQLImagesStore) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM images WHERE id = $1`
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
@ -64,7 +74,7 @@ func (s SQLImagesStore) Delete(ctx context.Context, id int64) error {
return nil
}
func (s SQLImagesStore) ActivateImage(ctx context.Context, id int64) error {
func (s *SQLImagesStore) ActivateImage(ctx context.Context, id int64) error {
query := `UPDATE images SET added = $1`
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
@ -77,3 +87,15 @@ func (s SQLImagesStore) ActivateImage(ctx context.Context, id int64) error {
return nil
}
func (s *SQLImagesStore) cleanupDeadImages(ctx context.Context, period time.Duration) {
for range time.Tick(period) {
query := `DELETE FROM images WHERE added = false AND expiration < $1`
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
s.db.ExecContext(ctx, query, time.Now())
}
// ignore any errors, just keep looping. Not much lost in a missed cleanup. A little extra temporary storage. Big woop. If it's an issue, decrease the cleanup period
}

View File

@ -23,6 +23,7 @@ func (s *SQLRolesStore) GetByName(ctx context.Context, name string) (*Role, erro
return role, nil
}
func (s *SQLRolesStore) GetById(ctx context.Context, id int64) (*Role, error) {
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()

View File

@ -29,7 +29,7 @@ func (s *SQLUsersStore) GetByID(ctx context.Context, id int64) (*User, error) {
&user.ID,
&user.Username,
&user.Email,
&user.Password.hash,
&user.Password,
&user.CreatedAt,
&user.Role.ID,
&user.Role.Name,
@ -66,7 +66,7 @@ func (s *SQLUsersStore) GetByEmail(ctx context.Context, email string) (*User, er
&user.ID,
&user.Username,
&user.Email,
&user.Password.hash,
&user.Password,
&user.CreatedAt,
&user.Role.ID,
&user.Role.Name,
@ -103,7 +103,7 @@ func (s *SQLUsersStore) GetByUsername(ctx context.Context, username string) (*Us
&user.ID,
&user.Username,
&user.Email,
&user.Password.hash,
&user.Password,
&user.CreatedAt,
&user.Role.ID,
&user.Role.Name,
@ -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 {
@ -195,7 +200,7 @@ func (s *SQLUsersStore) delete(ctx context.Context, id int64, tx *sql.Tx) error
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()
res, err := s.db.ExecContext(ctx, query, id)
res, err := tx.ExecContext(ctx, query, id)
if err != nil {
return err
}
@ -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

@ -8,10 +8,12 @@ import (
)
var (
ErrNotFound = errors.New("resource not found")
ErrConflict = errors.New("resource already exists")
ErrDuplicateEmail = errors.New("a user with that email already exists")
ErrDuplicateUsername = errors.New("a user with that username already exists")
ErrNotFound = errors.New("resource not found")
ErrConflict = errors.New("resource already exists")
ErrDuplicateEmail = errors.New("a user with that email already exists")
ErrDuplicateUsername = errors.New("a user with that username already exists")
ErrInvalidReceiptsQuery = errors.New("invalid receipts query")
ErrSearchTermTooLong = errors.New("search term too long")
)
type Storage struct {
@ -19,16 +21,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
@ -54,6 +64,7 @@ type Storage struct {
GetByID(ctx context.Context, id int64) (*Receipt, error)
Create(ctx context.Context, receipt *Receipt) error
Delete(ctx context.Context, id int64) error
GetForUser(ctx context.Context, userid int64) ([]*Receipt, error)
}
Images interface {
GetByID(context.Context, int64) (*Image, error)
@ -66,8 +77,8 @@ type Storage struct {
}
Groups interface {
GetByID(context.Context, int64) (*Group, error)
GetUserGroups(context.Context, int64) ([]*Group, error)
GetUsersInGroup(context.Context, int64) ([]*User, error)
GetUserGroups(context.Context, int64) (*UserGroups, error)
// GetUsersInGroup(context.Context, int64) ([]*User, error)
Create(context.Context, *Group) error
Delete(context.Context, int64) error
}
@ -75,6 +86,6 @@ type Storage struct {
func NewSQLRedisMinIOStorage(db *sql.DB) Storage {
return Storage{
Users: &SQLUsersStore{db},
// Users: &SQLUsersStore{db},
}
}