Updating storage and adding SQL DB schema

Unfinished

Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
This commit is contained in:
Ethan Wellenreiter 2025-05-02 20:26:03 -04:00
parent be7a54e7f4
commit f608f29842
12 changed files with 245 additions and 60 deletions

View File

@ -0,0 +1,73 @@
// Use DBML to define your database structure
// Docs: https://dbml.dbdiagram.io/docs
Table users {
id integer [primary key]
username varchar unique
email varchar unique
password varchar
created_at timestamp
is_active bool
role integer
personalgroup integer
}
Table groups {
id integer [primary key]
name varchar
owner integer
}
Table groupMembership {
membershipid integer [primary key]
groupid integer
userid integer
moderator bool
}
Table roles {
id integer [primary key]
name varchar
description varchar
level integer
}
Table reciepts {
id integer [primary key]
groupownerid integer
data nvarchar
}
// Table imageOwnership {
// ownershipid integer [primary key]
// receiptid integer
// imageid integer
// }
Table images {
id integer [primary key]
receiptid integer
created_at timestamp
path varchar
}
Ref: "users"."personalgroup" > "groups"."id"
Ref: "groups"."owner" > "users"."id"
Ref: "groups"."id" < "groupMembership"."groupid"
Ref: "groupMembership"."userid" < "users"."id"
Ref: "roles"."id" < "users"."role"
// Ref: "reciepts"."id" < "imageOwnership"."receiptid"
// Ref: "images"."id" < "imageOwnership"."imageid"
Ref: "reciepts"."id" < "images"."receiptid"
Ref: "groups"."id" < "reciepts"."groupownerid"

View File

@ -1 +0,0 @@
package auth_storage

View File

@ -1,42 +0,0 @@
package auth_storage
import (
"context"
"time"
)
type AuthStorage struct {
Users interface { // store user id, username, password(hashed+salted), role?
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?
Delete(ctx context.Context, id int64) error
UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error
CheckPass(ctx context.Context, name string, pass string) (bool, error)
SigninUser(ctx context.Context, name string, pass string) (bool, *User, error)
// ValidCredentials(ctx context.Context, user *User, pass string) (bool, error)
}
Sessions interface { // store just session tokens, and their corresponding user id
AddSession(ctx context.Context, userid int64) (token string, err error)
CheckSession(ctx context.Context, token string) (valid bool, userid int64, err error) // should also extend it by the lifespan if near the end of the time. maybe a 5 min window at the end?
RemoveSession(ctx context.Context, token string) error
// SetLifespan(ctx context.Context, token string, lf time.Time) error
}
CSRF interface {
AddCSRF(ctx context.Context, sessionToken string) (csrftoken string, err error)
CheckCSRF(ctx context.Context, sessionToken string, csrfToken string) (bool, error)
RemoveCSRF(ctx context.Context, sessionToken string) error
// CleanupCSRF()
}
Roles interface {
GetByName(context.Context, string) (*Role, error)
}
}

View File

@ -4,13 +4,13 @@ import (
"context"
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
// auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
)
type Storage struct {
Users interface {
Get(ctx context.Context, id int64) (*auth_storage.User, error)
Set(ctx context.Context, user *auth_storage.User) error
Get(ctx context.Context, id int64) (*storage.User, error)
Set(ctx context.Context, user *storage.User) error
Delete(ctx context.Context, userID int64)
}
Receipts interface {

View File

@ -6,7 +6,8 @@ import (
"fmt"
"time"
auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth"
// 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"
)
@ -16,7 +17,7 @@ type UserStore struct {
const UserExpTime = time.Minute
func (s *UserStore) Get(ctx context.Context, userID int64) (*auth_storage.User, error) {
func (s *UserStore) Get(ctx context.Context, userID int64) (*storage.User, error) {
cacheKey := fmt.Sprintf("user-%d", userID)
data, err := s.rdb.Get(ctx, cacheKey).Result()
@ -26,7 +27,7 @@ func (s *UserStore) Get(ctx context.Context, userID int64) (*auth_storage.User,
return nil, err
}
var user auth_storage.User
var user storage.User
if data != "" {
err := json.Unmarshal([]byte(data), &user)
if err != nil {
@ -37,7 +38,7 @@ func (s *UserStore) Get(ctx context.Context, userID int64) (*auth_storage.User,
return &user, nil
}
func (s *UserStore) Set(ctx context.Context, user *auth_storage.User) error {
func (s *UserStore) Set(ctx context.Context, user *storage.User) error {
cacheKey := fmt.Sprintf("user-%d", user.ID)
json, err := json.Marshal(user)

View File

@ -3,8 +3,8 @@ package storage
import "time"
type Receipt struct {
ID int64 `json:"id"`
Owner string `json:"username"`
ID int64 `json:"id"`
// Owner string `json:"username"`
OwnerID int64 `json:"user_id"`
ImageIDs []int64 `json:"image_ids"`
Data ReceiptData `json:"receipt_data"`

View File

@ -1,7 +1,8 @@
package auth_storage
package storage
import (
"context"
"time"
"git.ewellenr.ca/receipt_indexer/backend/internal/lcrypto"
"github.com/redis/go-redis/v9"
@ -15,8 +16,9 @@ import (
// should be set by the hasher
type redisCSRF struct {
rdb *redis.Client
hasher lcrypto.Hasher
rdb *redis.Client
hasher lcrypto.Hasher
expirationTime uint
}
func (r *redisCSRF) AddCSRF(ctx context.Context, sessionToken string) (csrftoken string, err error) {
@ -26,7 +28,7 @@ func (r *redisCSRF) AddCSRF(ctx context.Context, sessionToken string) (csrftoken
}
csrftoken = string(csrf)
if err = r.rdb.Set(ctx, sessionToken, csrftoken, 0).Err(); err != nil {
if err = r.rdb.Set(ctx, sessionToken, csrftoken, time.Duration(r.expirationTime)).Err(); err != nil {
return "", err
}
return csrftoken, nil
@ -43,6 +45,10 @@ func (r *redisCSRF) ValidCSRF(ctx context.Context, sessionToken string, csrfToke
return false, err
}
if err = r.rdb.ExpireXX(ctx, sessionToken, time.Duration(r.expirationTime)).Err(); err != nil {
return false, err
}
valid, err := lcrypto.CompareHash([]byte(csrfToken), []byte(storedcsrfToken))
if err != nil {
return false, err

View File

@ -0,0 +1,59 @@
package storage
import (
"context"
"time"
"git.ewellenr.ca/receipt_indexer/backend/internal/lcrypto"
"github.com/redis/go-redis/v9"
)
// game plan. store the session token/id and a salt. then, hash them to create the csrf token. give this csrf token out to the user. when the user ends a session, it ends the session but also deletes it from the csrf store
// const csrfSaltLength = 32
// const csrfTokenLength = 128
// should be set by the hasher
type redisSession struct {
rdb *redis.Client
sessionTokenLength uint
expirationTime uint
}
func (r *redisSession) AddSession(ctx context.Context, userid int64) (token string, err error) {
temp, err := lcrypto.GenerateRandomBytes(r.sessionTokenLength)
if err != nil {
return "", err
}
token = string(temp)
err = r.rdb.Set(ctx, token, userid, time.Duration(r.expirationTime)).Err()
if err != nil {
return "", err
}
return token, err
}
func (r *redisSession) GetSession(ctx context.Context, token string) (valid bool, userid int64, err error) { // should also extend it by the lifespan if near the end of the time. maybe a 5 min window at the end?
userid, err = r.rdb.Get(ctx, token).Int64()
if err == redis.Nil {
valid = false
userid = -1
} else if err != nil {
return false, -1, err
} else {
valid = true
}
err = r.rdb.ExpireXX(ctx, token, time.Duration(r.expirationTime)).Err()
if err != nil {
return false, -1, err
}
return valid, userid, err
}
func (r *redisSession) RemoveSession(ctx context.Context, token string) error {
return r.rdb.Del(ctx, token).Err()
}

View File

@ -1,4 +1,4 @@
package auth_storage
package storage
type Role struct {
ID int64 `json:"id"`

View File

@ -0,0 +1,51 @@
package storage
import (
"context"
"database/sql"
"time"
)
type SQLUsersStore struct {
db *sql.DB
}
func (s *SQLUsersStore) GetByID(ctx context.Context, id int64) (*User, error) {
return nil, nil
}
func (s *SQLUsersStore) GetByEmail(context.Context, string) (*User, error) {
return nil, nil
}
func (s *SQLUsersStore) GetByUsername(context.Context, string) (*User, error) {
return nil, nil
}
func (s *SQLUsersStore) Create(context.Context, *User) error { // create a non-exported create function which does take in the tx
return nil
}
func (s *SQLUsersStore) CreateAndInvite(ctx context.Context, user *User, token string, exp time.Duration) error { // figure this out
return nil
}
func (s *SQLUsersStore) Activate(context.Context, string) error { // what does this do?
return nil
}
func (s *SQLUsersStore) Delete(ctx context.Context, id int64) error {
return nil
}
func (s *SQLUsersStore) UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error {
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) {
return false, nil, nil
}

View File

@ -3,9 +3,44 @@ package storage
import (
"context"
"database/sql"
"time"
)
type Storage struct {
Users interface { // store user id, username, password(hashed+salted), role?
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?
Delete(ctx context.Context, id int64) error
UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error
CheckPass(ctx context.Context, name string, pass string) (bool, error)
SigninUser(ctx context.Context, name string, pass string) (bool, *User, error)
// ValidCredentials(ctx context.Context, user *User, pass string) (bool, error)
}
Sessions interface { // store just session tokens, and their corresponding user id
AddSession(ctx context.Context, userid int64) (token string, err error)
GetSession(ctx context.Context, token string) (valid bool, userid int64, err error) // extends it's expiry
RemoveSession(ctx context.Context, token string) error
// SetLifespan(ctx context.Context, token string, lf time.Time) error
}
CSRF interface {
AddCSRF(ctx context.Context, sessionToken string) (csrftoken string, err error)
CheckCSRF(ctx context.Context, sessionToken string, csrfToken string) (bool, error)
RemoveCSRF(ctx context.Context, sessionToken string) error
// CleanupCSRF()
}
Roles interface {
GetByName(context.Context, string) (*Role, error)
}
Receipts interface {
GetByID(context.Context, int64) (*Receipt, error)
}
@ -15,11 +50,14 @@ type Storage struct {
}
Groups interface {
GetByID(context.Context, int64)
GetUserGroups(context.Context, int64)
}
}
func NewSQLStorage(db *sql.DB) Storage {
return Storage{}
func NewSQLRedisMinIOStorage(db *sql.DB) Storage {
return Storage{
Users: &SQLUsersStore{db},
}
}
func withTx(db *sql.DB, ctx context.Context, fn func(*sql.Tx) error) error {

View File

@ -1,4 +1,4 @@
package auth_storage
package storage
import (
"errors"