feat: initial datastructure #3
98
Readme.md
98
Readme.md
@@ -1,98 +1,12 @@
|
||||
|
||||
# Web-App-Template
|
||||
# SpendSparrow
|
||||
|
||||
A basic template with authentication to easily host on a VPC.
|
||||
SpendSparrow is a web app to keep track of expenses and income. It is very opinionated by keeping an keen eye on disciplin of it's users. Every Expense needs to be mapped to a Piggy Bank. For emergencies, funds can be moved between Piggy Banks.
|
||||
|
||||
## Features
|
||||
## Design priciples
|
||||
|
||||
This template includes everything essential to build an app. It includes the following features:
|
||||
|
||||
- Authentication: Users can login, logout, register and reset their password. (for increased security TOTP is planned aswell.)
|
||||
- Observability: The stack contains an Grafana+Prometheus instance for basic monitoring. You are able to add alerts and get notified on your phone.
|
||||
- Mail: You are able to send mail with SMTP. You still need an external Mail Server, but a guide on how to set that up with a custom domain is included.
|
||||
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains.
|
||||
- Stack: Tailwindcss + HTMX + GO Backend with templ and sqlite
|
||||
|
||||
|
||||
## Architecture Design Decisions
|
||||
|
||||
### Authentication
|
||||
|
||||
Authentication is a broad topic. Many people think you should not consider implementing authentication yourself. On the other hand, If only security experts are allowed to write software, what does that result in? I'm going to explain my criterions and afterwards take a decision.
|
||||
|
||||
There are a few restrictions I would like to contain:
|
||||
- I want this template do as much as as possible without relying on external services. This way the setup cost and dependencies can be minimized.
|
||||
- It should still be possible to run on a small VPC (2vcpu, 2GB).
|
||||
- It should be as secure as possible
|
||||
|
||||
I determined 4 options:
|
||||
1. Implement the authentication myself
|
||||
2. Using OAuth2 with Keycloak
|
||||
3. Using OAuth2 with Google and Apple
|
||||
4. Firebase, Clerk, etc.
|
||||
|
||||
|
||||
#### 1. Implement the authentication myself
|
||||
|
||||
It's always possible to implement it myself. The topic of authentication is something special though.
|
||||
|
||||
Pros:
|
||||
- Great Cheat cheets from OWASP
|
||||
- No adittional configuration or services needed
|
||||
- Great learning experience on the topic "security"
|
||||
Cons:
|
||||
- Great attack vector
|
||||
- Introcution of vlunerabillities is possible
|
||||
- No DDOS protection
|
||||
|
||||
#### 2. Using OAuth2 with Google and Apple
|
||||
|
||||
Instead of implementing authentication from scratch, an external OAuth2 provider is embedded into the application.
|
||||
|
||||
Pros:
|
||||
- The Systems of BigTech are probably safer. They have security experts employed.
|
||||
- The other external system is responsible to prevent credential stuffing attacks, etc.
|
||||
- Users don't have to create new credentials
|
||||
Cons:
|
||||
- High dependency on those providers
|
||||
- Single Point of failure (If your account is banned, your application access get's lost as well.)
|
||||
- It's possible that these providers ban the whole application (All users lose access)
|
||||
- There still needs to be implemented some logic
|
||||
- Full application integration can be difficult
|
||||
|
||||
#### 3. Using OAuth2 with Keycloak
|
||||
|
||||
This option is almost identical with the previois one, but the provider is self hosted.
|
||||
|
||||
Pros:
|
||||
- Indipendent from 3rd party providers
|
||||
- The credentials are stored safly
|
||||
Cons:
|
||||
- Self hosted (no DDOS protection, etc.)
|
||||
- There still needs to be implemented some logic server side
|
||||
- Full application integration can be difficult
|
||||
|
||||
|
||||
#### 4. Firebase, Clerk, etc.
|
||||
|
||||
Users can sign in with a seperate sdk on your website
|
||||
|
||||
Pros:
|
||||
- Safe and Sound authentication
|
||||
Cons:
|
||||
- Dependent on those providers / adittional setup needed
|
||||
- Application can be banned
|
||||
- Still some integration code needed
|
||||
|
||||
#### Decision
|
||||
|
||||
I've decided on implementing authentication myself, as this is a great learning opportunity. It may not be as secure as other solutions, but if I keep tighly to the OWASP recommendations, it should should good enough.
|
||||
|
||||
|
||||
### Email
|
||||
|
||||
For Email verification, etc. a mail server is needed, that can send a whole lot of mails. Aditionally, a mail account is needed for incoming emails. I thought about self hosting, but unfortunatly this is a hastle to maintain. Not only you have to setup a mail server, which is not as easy as it sounds, you also have to "register" your mail server for diffrent providers. Otherwise you are not able to send and receive emails.
|
||||
|
||||
In order to not vendor lock in, I decided to use an SMTP relay in favor of a vendor specific API. I chose brevo.com. They have a generous free tier of 300 mails per day. You can either upgrade to a monthly plan 10$ for 20k mails or buy credits for 30$ for 5k mails.
|
||||
The State of the application can always be calculated on the fly. Even though it is not an Event Streaming Application, it is still important to be able to recalculate historic data.
|
||||
It may be applicable to do some sort of monthly snapshots to speed up calculations, but this will be only done if database queries become a bottleneck.
|
||||
|
||||
This applications uses as little dependencies as feasible, especially on the front end.
|
||||
|
||||
|
||||
118
db/account.go
Normal file
118
db/account.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"spend-sparrow/log"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Account interface {
|
||||
Insert(account *types.Account) error
|
||||
Update(account *types.Account) error
|
||||
GetAll(groupId uuid.UUID) ([]*types.Account, error)
|
||||
Get(groupId uuid.UUID, id uuid.UUID) (*types.Account, error)
|
||||
Delete(id uuid.UUID) error
|
||||
}
|
||||
|
||||
type AccountSqlite struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewAccountSqlite(db *sqlx.DB) *AccountSqlite {
|
||||
return &AccountSqlite{db: db}
|
||||
}
|
||||
|
||||
func (db AccountSqlite) Insert(account *types.Account) error {
|
||||
|
||||
_, err := db.db.Exec(`
|
||||
INSERT INTO account (id, group_id, name, current_balance, oink_balance, created_at, created_by)
|
||||
VALUES (?,?,?,?,?,?,?)`, account.Id, account.GroupId, 0, 0, account.CreatedAt, account.CreatedBy)
|
||||
if err != nil {
|
||||
log.Error("Error inserting account: %v", err)
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db AccountSqlite) Update(account *types.Account) error {
|
||||
|
||||
_, err := db.db.Exec(`
|
||||
UPDATE account
|
||||
name = ?,
|
||||
current_balance = ?,
|
||||
last_transaction = ?,
|
||||
oink_balance = ?,
|
||||
updated_at = ?,
|
||||
updated_by = ?,
|
||||
WHERE id = ?
|
||||
AND group_id = ?`, account.Name, account.CurrentBalance, account.LastTransaction, account.OinkBalance, account.UpdatedAt, account.UpdatedBy, account.Id, account.GroupId)
|
||||
if err != nil {
|
||||
log.Error("Error updating account: %v", err)
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db AccountSqlite) GetAll(groupId uuid.UUID) ([]*types.Account, error) {
|
||||
|
||||
accounts := make([]*types.Account, 0)
|
||||
err := db.db.Select(&accounts, `
|
||||
SELECT
|
||||
id, name,
|
||||
current_balance, last_transaction, oink_balance,
|
||||
created_at, created_by, updated_at, updated_by
|
||||
FROM account
|
||||
WHERE group_id = ?
|
||||
ORDER BY name`, groupId)
|
||||
if err != nil {
|
||||
log.Error("Could not getAll accounts: %v", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (db AccountSqlite) Get(groupId uuid.UUID, id uuid.UUID) (*types.Account, error) {
|
||||
|
||||
account := &types.Account{}
|
||||
err := db.db.Get(account, `
|
||||
SELECT
|
||||
id, name,
|
||||
current_balance, last_transaction, oink_balance,
|
||||
created_at, created_by, updated_at, updated_by
|
||||
FROM account
|
||||
WHERE group_id = ?
|
||||
AND id = ?`, groupId, id)
|
||||
if err != nil {
|
||||
log.Error("Could not get accounts: %v", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (db AccountSqlite) Delete(id uuid.UUID) error {
|
||||
|
||||
res, err := db.db.Exec("DELETE FROM account WHERE id = ?", id)
|
||||
if err != nil {
|
||||
log.Error("Error deleting account: %v", err)
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
log.Error("Error deleting account, getting rows affected: %v", err)
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
15
db/auth.go
15
db/auth.go
@@ -5,16 +5,11 @@ import (
|
||||
"spend-sparrow/types"
|
||||
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("value not found")
|
||||
ErrAlreadyExists = errors.New("row already exists")
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Auth interface {
|
||||
@@ -38,10 +33,10 @@ type Auth interface {
|
||||
}
|
||||
|
||||
type AuthSqlite struct {
|
||||
db *sql.DB
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewAuthSqlite(db *sql.DB) *AuthSqlite {
|
||||
func NewAuthSqlite(db *sqlx.DB) *AuthSqlite {
|
||||
return &AuthSqlite{db: db}
|
||||
}
|
||||
|
||||
@@ -140,10 +135,10 @@ func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM workout WHERE user_id = ?", userId)
|
||||
_, err = tx.Exec("DELETE FROM account WHERE group_id = ?", userId)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
log.Error("Could not delete workouts: %v", err)
|
||||
log.Error("Could not delete accounts: %v", err)
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"spend-sparrow/types"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setupDb(t *testing.T) *sql.DB {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
func setupDb(t *testing.T) *sqlx.DB {
|
||||
db, err := sqlx.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening database: %v", err)
|
||||
}
|
||||
|
||||
@@ -4,16 +4,21 @@ import (
|
||||
"spend-sparrow/log"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func RunMigrations(db *sql.DB, pathPrefix string) error {
|
||||
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||
var (
|
||||
ErrNotFound = errors.New("The value does not exist.")
|
||||
ErrAlreadyExists = errors.New("row already exists")
|
||||
)
|
||||
|
||||
func RunMigrations(db *sqlx.DB, pathPrefix string) error {
|
||||
driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{})
|
||||
if err != nil {
|
||||
log.Error("Could not create Migration instance: %v", err)
|
||||
return types.ErrInternal
|
||||
@@ -38,4 +43,3 @@ func RunMigrations(db *sql.DB, pathPrefix string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
119
db/workout.go
119
db/workout.go
@@ -1,119 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"spend-sparrow/log"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWorkoutNotExists = errors.New("Workout does not exist")
|
||||
)
|
||||
|
||||
type WorkoutDb interface {
|
||||
InsertWorkout(userId uuid.UUID, workout *WorkoutInsert) (*Workout, error)
|
||||
GetWorkouts(userId uuid.UUID) ([]Workout, error)
|
||||
DeleteWorkout(userId uuid.UUID, rowId int) error
|
||||
}
|
||||
|
||||
type WorkoutDbSqlite struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewWorkoutDbSqlite(db *sql.DB) *WorkoutDbSqlite {
|
||||
return &WorkoutDbSqlite{db: db}
|
||||
}
|
||||
|
||||
type WorkoutInsert struct {
|
||||
Date time.Time
|
||||
Type string
|
||||
Sets int
|
||||
Reps int
|
||||
}
|
||||
|
||||
type Workout struct {
|
||||
RowId int
|
||||
Date time.Time
|
||||
Type string
|
||||
Sets int
|
||||
Reps int
|
||||
}
|
||||
|
||||
func NewWorkoutInsert(date time.Time, workoutType string, sets int, reps int) *WorkoutInsert {
|
||||
return &WorkoutInsert{Date: date, Type: workoutType, Sets: sets, Reps: reps}
|
||||
}
|
||||
|
||||
func NewWorkoutFromInsert(rowId int, workoutInsert *WorkoutInsert) *Workout {
|
||||
return &Workout{RowId: rowId, Date: workoutInsert.Date, Type: workoutInsert.Type, Sets: workoutInsert.Sets, Reps: workoutInsert.Reps}
|
||||
}
|
||||
|
||||
func (db WorkoutDbSqlite) InsertWorkout(userId uuid.UUID, workout *WorkoutInsert) (*Workout, error) {
|
||||
var rowId int
|
||||
err := db.db.QueryRow(`
|
||||
INSERT INTO workout (user_id, date, type, sets, reps)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING rowid`, userId, workout.Date, workout.Type, workout.Sets, workout.Reps).Scan(&rowId)
|
||||
if err != nil {
|
||||
log.Error("Error inserting workout: %v", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return NewWorkoutFromInsert(rowId, workout), nil
|
||||
}
|
||||
|
||||
func (db WorkoutDbSqlite) GetWorkouts(userId uuid.UUID) ([]Workout, error) {
|
||||
|
||||
rows, err := db.db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ? ORDER BY date desc", userId)
|
||||
if err != nil {
|
||||
log.Error("Could not get workouts: %v", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
var workouts = make([]Workout, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
workout Workout
|
||||
date string
|
||||
)
|
||||
|
||||
err = rows.Scan(&workout.RowId, &date, &workout.Type, &workout.Sets, &workout.Reps)
|
||||
if err != nil {
|
||||
log.Error("Could not scan workout: %v", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
workout.Date, err = time.Parse("2006-01-02 15:04:05-07:00", date)
|
||||
if err != nil {
|
||||
log.Error("Could not parse date: %v", err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
workouts = append(workouts, workout)
|
||||
}
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
func (db WorkoutDbSqlite) DeleteWorkout(userId uuid.UUID, rowId int) error {
|
||||
|
||||
res, err := db.db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", userId, rowId)
|
||||
if err != nil {
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return ErrWorkoutNotExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/a-h/templ v0.3.865
|
||||
github.com/golang-migrate/migrate/v4 v4.18.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -17,6 +17,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
|
||||
126
handler/account.go
Normal file
126
handler/account.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"spend-sparrow/handler/middleware"
|
||||
"spend-sparrow/service"
|
||||
"spend-sparrow/template/account"
|
||||
"spend-sparrow/utils"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Account interface {
|
||||
Handle(router *http.ServeMux)
|
||||
}
|
||||
|
||||
type AccountImpl struct {
|
||||
service service.Account
|
||||
auth service.Auth
|
||||
render *Render
|
||||
}
|
||||
|
||||
func NewAccount(service service.Account, auth service.Auth, render *Render) Account {
|
||||
return AccountImpl{
|
||||
service: service,
|
||||
auth: auth,
|
||||
render: render,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler AccountImpl) Handle(router *http.ServeMux) {
|
||||
router.Handle("/account", handler.handleAccountPage())
|
||||
// router.Handle("POST /account", handler.handleAddAccount())
|
||||
// router.Handle("GET /account", handler.handleGetAccount())
|
||||
// router.Handle("DELETE /account/{id}", handler.handleDeleteAccount())
|
||||
}
|
||||
|
||||
func (handler AccountImpl) handleAccountPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
comp := account.AccountListComp(nil)
|
||||
handler.render.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
// func (handler AccountImpl) handleAddAccount() http.HandlerFunc {
|
||||
// return func(w http.ResponseWriter, r *http.Request) {
|
||||
// user := middleware.GetUser(r)
|
||||
// if user == nil {
|
||||
// utils.DoRedirect(w, r, "/auth/signin")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var dateStr = r.FormValue("date")
|
||||
// var typeStr = r.FormValue("type")
|
||||
// var setsStr = r.FormValue("sets")
|
||||
// var repsStr = r.FormValue("reps")
|
||||
//
|
||||
// wo := service.NewAccountDto("", dateStr, typeStr, setsStr, repsStr)
|
||||
// wo, err := handler.service.AddAccount(user, wo)
|
||||
// if err != nil {
|
||||
// utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest)
|
||||
// http.Error(w, "Invalid input values", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// wor := account.Account{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps}
|
||||
//
|
||||
// comp := account.AccountItemComp(wor, true)
|
||||
// handler.render.Render(r, w, comp)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func (handler AccountImpl) handleGetAccount() http.HandlerFunc {
|
||||
// return func(w http.ResponseWriter, r *http.Request) {
|
||||
// user := middleware.GetUser(r)
|
||||
// if user == nil {
|
||||
// utils.DoRedirect(w, r, "/auth/signin")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// workouts, err := handler.service.GetAccounts(user)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// wos := make([]*types.Account, 0)
|
||||
// for _, wo := range workouts {
|
||||
// wos = append(wos, *types.Account{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps})
|
||||
// }
|
||||
//
|
||||
// comp := account.AccountListComp(wos)
|
||||
// handler.render.Render(r, w, comp)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func (handler AccountImpl) handleDeleteAccount() http.HandlerFunc {
|
||||
// return func(w http.ResponseWriter, r *http.Request) {
|
||||
// user := middleware.GetUser(r)
|
||||
// if user == nil {
|
||||
// utils.DoRedirect(w, r, "/auth/signin")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// rowId := r.PathValue("id")
|
||||
// if rowId == "" {
|
||||
// utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// rowIdInt, err := strconv.Atoi(rowId)
|
||||
// if err != nil {
|
||||
// utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// err = handler.service.DeleteAccount(user, rowIdInt)
|
||||
// if err != nil {
|
||||
// utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,129 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"spend-sparrow/handler/middleware"
|
||||
"spend-sparrow/service"
|
||||
"spend-sparrow/template/workout"
|
||||
"spend-sparrow/utils"
|
||||
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Workout interface {
|
||||
Handle(router *http.ServeMux)
|
||||
}
|
||||
|
||||
type WorkoutImpl struct {
|
||||
service service.Workout
|
||||
auth service.Auth
|
||||
render *Render
|
||||
}
|
||||
|
||||
func NewWorkout(service service.Workout, auth service.Auth, render *Render) Workout {
|
||||
return WorkoutImpl{
|
||||
service: service,
|
||||
auth: auth,
|
||||
render: render,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler WorkoutImpl) Handle(router *http.ServeMux) {
|
||||
router.Handle("/workout", handler.handleWorkoutPage())
|
||||
router.Handle("POST /api/workout", handler.handleAddWorkout())
|
||||
router.Handle("GET /api/workout", handler.handleGetWorkout())
|
||||
router.Handle("DELETE /api/workout/{id}", handler.handleDeleteWorkout())
|
||||
}
|
||||
|
||||
func (handler WorkoutImpl) handleWorkoutPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
currentDate := time.Now().Format("2006-01-02")
|
||||
comp := workout.WorkoutComp(currentDate)
|
||||
handler.render.RenderLayout(r, w, comp, user)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
var dateStr = r.FormValue("date")
|
||||
var typeStr = r.FormValue("type")
|
||||
var setsStr = r.FormValue("sets")
|
||||
var repsStr = r.FormValue("reps")
|
||||
|
||||
wo := service.NewWorkoutDto("", dateStr, typeStr, setsStr, repsStr)
|
||||
wo, err := handler.service.AddWorkout(user, wo)
|
||||
if err != nil {
|
||||
utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest)
|
||||
http.Error(w, "Invalid input values", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wor := workout.Workout{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps}
|
||||
|
||||
comp := workout.WorkoutItemComp(wor, true)
|
||||
handler.render.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler WorkoutImpl) handleGetWorkout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
workouts, err := handler.service.GetWorkouts(user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wos := make([]workout.Workout, 0)
|
||||
for _, wo := range workouts {
|
||||
wos = append(wos, workout.Workout{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps})
|
||||
}
|
||||
|
||||
comp := workout.WorkoutListComp(wos)
|
||||
handler.render.Render(r, w, comp)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler WorkoutImpl) handleDeleteWorkout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
utils.DoRedirect(w, r, "/auth/signin")
|
||||
return
|
||||
}
|
||||
|
||||
rowId := r.PathValue("id")
|
||||
if rowId == "" {
|
||||
utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rowIdInt, err := strconv.Atoi(rowId)
|
||||
if err != nil {
|
||||
utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.service.DeleteWorkout(user, rowIdInt)
|
||||
if err != nil {
|
||||
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
16
main.go
16
main.go
@@ -9,7 +9,6 @@ import (
|
||||
"spend-sparrow/types"
|
||||
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", "./data.db")
|
||||
db, err := sqlx.Open("sqlite3", "./data.db")
|
||||
if err != nil {
|
||||
log.Fatal("Could not open Database data.db: %v", err)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func main() {
|
||||
run(context.Background(), db, os.Getenv)
|
||||
}
|
||||
|
||||
func run(ctx context.Context, database *sql.DB, env func(string) string) {
|
||||
func run(ctx context.Context, database *sqlx.DB, env func(string) string) {
|
||||
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
@@ -100,26 +100,26 @@ func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
func createHandler(d *sql.DB, serverSettings *types.Settings) http.Handler {
|
||||
func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
||||
var router = http.NewServeMux()
|
||||
|
||||
authDb := db.NewAuthSqlite(d)
|
||||
workoutDb := db.NewWorkoutDbSqlite(d)
|
||||
accountDb := db.NewAccountSqlite(d)
|
||||
|
||||
randomService := service.NewRandomImpl()
|
||||
clockService := service.NewClockImpl()
|
||||
mailService := service.NewMailImpl(serverSettings)
|
||||
|
||||
authService := service.NewAuthImpl(authDb, randomService, clockService, mailService, serverSettings)
|
||||
workoutService := service.NewWorkoutImpl(workoutDb, randomService, clockService, mailService, serverSettings)
|
||||
accountService := service.NewAccountImpl(accountDb, randomService, clockService, serverSettings)
|
||||
|
||||
render := handler.NewRender()
|
||||
indexHandler := handler.NewIndex(authService, render)
|
||||
authHandler := handler.NewAuth(authService, render)
|
||||
workoutHandler := handler.NewWorkout(workoutService, authService, render)
|
||||
accountHandler := handler.NewAccount(accountService, authService, render)
|
||||
|
||||
indexHandler.Handle(router)
|
||||
workoutHandler.Handle(router)
|
||||
accountHandler.Handle(router)
|
||||
authHandler.Handle(router)
|
||||
|
||||
// Serve static files (CSS, JS and images)
|
||||
|
||||
10
main_test.go
10
main_test.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"spend-sparrow/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
@@ -928,7 +928,7 @@ func TestIntegrationAuth(t *testing.T) {
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, rows)
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM workout WHERE user_id = ?", userId).Scan(&rows)
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM account WHERE group_id = ?", userId).Scan(&rows)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, rows)
|
||||
})
|
||||
@@ -1602,7 +1602,7 @@ func TestIntegrationAuth(t *testing.T) {
|
||||
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
|
||||
assert.Nil(t, err)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/workout", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/account", nil)
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", "id="+sessionId)
|
||||
resp, err := httpClient.Do(req)
|
||||
@@ -1624,11 +1624,11 @@ func findCookie(resp *http.Response, name string) *http.Cookie {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupIntegrationTest(t *testing.T) (db *sql.DB, basePath string, ctx context.Context) {
|
||||
func setupIntegrationTest(t *testing.T) (db *sqlx.DB, basePath string, ctx context.Context) {
|
||||
ctx, done := context.WithCancel(context.Background())
|
||||
t.Cleanup(done)
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
db, err := sqlx.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not open Database data.db: %v", err)
|
||||
}
|
||||
|
||||
@@ -35,10 +35,3 @@ CREATE TABLE token (
|
||||
expires_at DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE workout (
|
||||
user_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
sets INTEGER NOT NULL,
|
||||
reps INTEGER NOT NULL
|
||||
);
|
||||
|
||||
17
migration/002_account.up.sql
Normal file
17
migration/002_account.up.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
CREATE TABLE account (
|
||||
id TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
group_id TEXT NOT NULL,
|
||||
|
||||
name TEXT NOT NULL,
|
||||
|
||||
current_balance int64 NOT NULL,
|
||||
last_transaction DATETIME,
|
||||
oink_balance int64 NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
updated_at DATETIME,
|
||||
updated_by TEXT
|
||||
) WITHOUT ROWID;
|
||||
|
||||
158
service/account.go
Normal file
158
service/account.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"spend-sparrow/db"
|
||||
"spend-sparrow/log"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
||||
)
|
||||
|
||||
type Account interface {
|
||||
Add(user *types.User, name string) (*types.Account, error)
|
||||
Update(user *types.User, id uuid.UUID, name string) (*types.Account, error)
|
||||
Get(user *types.User) ([]*types.Account, error)
|
||||
Delete(user *types.User, id uuid.UUID) error
|
||||
}
|
||||
|
||||
type AccountImpl struct {
|
||||
db db.Account
|
||||
clock Clock
|
||||
random Random
|
||||
settings *types.Settings
|
||||
}
|
||||
|
||||
func NewAccountImpl(db db.Account, random Random, clock Clock, settings *types.Settings) Account {
|
||||
return AccountImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: NewRandomImpl(),
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (service AccountImpl) Add(user *types.User, name string) (*types.Account, error) {
|
||||
if user == nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
newId, err := service.random.UUID()
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
err = service.validateAccount(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account := &types.Account{
|
||||
Id: newId,
|
||||
GroupId: user.Id,
|
||||
|
||||
Name: name,
|
||||
|
||||
CurrentBalance: 0,
|
||||
LastTransaction: nil,
|
||||
OinkBalance: 0,
|
||||
|
||||
CreatedAt: service.clock.Now(),
|
||||
CreatedBy: user.Id,
|
||||
UpdatedAt: nil,
|
||||
UpdatedBy: nil,
|
||||
}
|
||||
|
||||
err = service.db.Insert(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
savedAccount, err := service.db.Get(user.Id, newId)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNotFound) {
|
||||
log.Error("Account not found after insert: %v", err)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
return savedAccount, nil
|
||||
}
|
||||
|
||||
func (service AccountImpl) Update(user *types.User, id uuid.UUID, name string) (*types.Account, error) {
|
||||
if user == nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
err := service.validateAccount(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account, err := service.db.Get(user.Id, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timestamp := service.clock.Now()
|
||||
account.Name = name
|
||||
account.UpdatedAt = ×tamp
|
||||
account.UpdatedBy = &user.Id
|
||||
|
||||
err = service.db.Update(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (service AccountImpl) Get(user *types.User) ([]*types.Account, error) {
|
||||
|
||||
if user == nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
accounts, err := service.db.GetAll(user.GroupId)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (service AccountImpl) Delete(user *types.User, id uuid.UUID) error {
|
||||
if user == nil {
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
account, err := service.db.Get(user.GroupId, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if account.GroupId != user.GroupId {
|
||||
return types.ErrUnauthorized
|
||||
}
|
||||
|
||||
err = service.db.Delete(account.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service AccountImpl) validateAccount(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("Please enter a value for the \"name\" field.")
|
||||
} else if !safeInputRegex.MatchString(name) {
|
||||
return errors.New("Please use only letters, dashes or numbers for \"name\".")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
8
service/money.go
Normal file
8
service/money.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package service
|
||||
|
||||
type MoneyImpl struct {
|
||||
}
|
||||
|
||||
func NewMoneyImpl() *MoneyImpl {
|
||||
return &MoneyImpl{}
|
||||
}
|
||||
80
service/money_test.go
Normal file
80
service/money_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMoneyCalculation(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("should calculate correct oink balance", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// underTest := NewMoneyImpl()
|
||||
//
|
||||
// // GIVEN
|
||||
// timestamp := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||
//
|
||||
// groupId := uuid.New()
|
||||
//
|
||||
// account := types.Account{
|
||||
// Id: uuid.New(),
|
||||
// GroupId: groupId,
|
||||
//
|
||||
// Type: "Bank",
|
||||
// Name: "Bank",
|
||||
//
|
||||
// CurrentBalance: 0,
|
||||
// LastTransaction: time.Time{},
|
||||
// OinkBalance: 0,
|
||||
// }
|
||||
//
|
||||
// // The PiggyBank is a fictional account. The money it "holds" is actually in the Account
|
||||
// piggyBank := types.PiggyBank{
|
||||
// Id: uuid.New(),
|
||||
// GroupId: groupId,
|
||||
//
|
||||
// AccountId: account.Id,
|
||||
// Name: "Car",
|
||||
//
|
||||
// CurrentBalance: 0,
|
||||
// }
|
||||
//
|
||||
// savingsPlan := types.SavingsPlan{
|
||||
// Id: uuid.New(),
|
||||
// GroupId: groupId,
|
||||
// PiggyBankId: piggyBank.Id,
|
||||
//
|
||||
// MonthlySaving: 10,
|
||||
//
|
||||
// ValidFrom: timestamp,
|
||||
// }
|
||||
//
|
||||
// transaction1 := types.Transaction{
|
||||
// Id: uuid.New(),
|
||||
// GroupId: groupId,
|
||||
//
|
||||
// AccountId: account.Id,
|
||||
//
|
||||
// Value: 20,
|
||||
// Timestamp: timestamp,
|
||||
// }
|
||||
//
|
||||
// transaction2 := types.Transaction{
|
||||
// Id: uuid.New(),
|
||||
// GroupId: groupId,
|
||||
//
|
||||
// AccountId: account.Id,
|
||||
// PiggyBankId: &piggyBank.Id,
|
||||
//
|
||||
// Value: -1,
|
||||
// Timestamp: timestamp.Add(1 * time.Hour),
|
||||
// }
|
||||
//
|
||||
// // WHEN
|
||||
// actual, err := underTest.CalculateAllBalancesInTime(account, piggyBank, savingsPlan, []types.Transaction{transaction1, transaction2})
|
||||
//
|
||||
// // THEN
|
||||
// assert.Nil(t, err)
|
||||
// assert.ElementsMatch(t, expected, actual)
|
||||
})
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"spend-sparrow/db"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Workout interface {
|
||||
AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error)
|
||||
DeleteWorkout(user *types.User, rowId int) error
|
||||
GetWorkouts(user *types.User) ([]*WorkoutDto, error)
|
||||
}
|
||||
|
||||
type WorkoutImpl struct {
|
||||
db db.WorkoutDb
|
||||
random Random
|
||||
clock Clock
|
||||
mail Mail
|
||||
settings *types.Settings
|
||||
}
|
||||
|
||||
func NewWorkoutImpl(db db.WorkoutDb, random Random, clock Clock, mail Mail, settings *types.Settings) Workout {
|
||||
return WorkoutImpl{
|
||||
db: db,
|
||||
random: random,
|
||||
clock: clock,
|
||||
mail: mail,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
type WorkoutDto struct {
|
||||
RowId string
|
||||
Date string
|
||||
Type string
|
||||
Sets string
|
||||
Reps string
|
||||
}
|
||||
|
||||
func NewWorkoutDtoFromDb(workout *db.Workout) *WorkoutDto {
|
||||
return &WorkoutDto{
|
||||
RowId: strconv.Itoa(workout.RowId),
|
||||
Date: renderDate(workout.Date),
|
||||
Type: workout.Type,
|
||||
Sets: strconv.Itoa(workout.Sets),
|
||||
Reps: strconv.Itoa(workout.Reps),
|
||||
}
|
||||
}
|
||||
func NewWorkoutDto(rowId string, date string, workoutType string, sets string, reps string) *WorkoutDto {
|
||||
return &WorkoutDto{
|
||||
RowId: rowId,
|
||||
Date: date,
|
||||
Type: workoutType,
|
||||
Sets: sets,
|
||||
Reps: reps,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInputValues = errors.New("invalid input values")
|
||||
)
|
||||
|
||||
func (service WorkoutImpl) AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error) {
|
||||
|
||||
if workoutDto.Date == "" || workoutDto.Type == "" || workoutDto.Sets == "" || workoutDto.Reps == "" {
|
||||
return nil, ErrInputValues
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02", workoutDto.Date)
|
||||
if err != nil {
|
||||
return nil, ErrInputValues
|
||||
}
|
||||
|
||||
sets, err := strconv.Atoi(workoutDto.Sets)
|
||||
if err != nil {
|
||||
return nil, ErrInputValues
|
||||
}
|
||||
|
||||
reps, err := strconv.Atoi(workoutDto.Reps)
|
||||
if err != nil {
|
||||
return nil, ErrInputValues
|
||||
}
|
||||
|
||||
workoutInsert := db.NewWorkoutInsert(date, workoutDto.Type, sets, reps)
|
||||
|
||||
workout, err := service.db.InsertWorkout(user.Id, workoutInsert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewWorkoutDtoFromDb(workout), nil
|
||||
}
|
||||
|
||||
func (service WorkoutImpl) DeleteWorkout(user *types.User, rowId int) error {
|
||||
if user == nil {
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
return service.db.DeleteWorkout(user.Id, rowId)
|
||||
}
|
||||
|
||||
func (service WorkoutImpl) GetWorkouts(user *types.User) ([]*WorkoutDto, error) {
|
||||
if user == nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
workouts, err := service.db.GetWorkouts(user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for _, workout := range workouts {
|
||||
// workout.Date = renderDate(workout.Date)
|
||||
// }
|
||||
|
||||
workoutsDto := make([]*WorkoutDto, len(workouts))
|
||||
for i, workout := range workouts {
|
||||
workoutsDto[i] = NewWorkoutDtoFromDb(&workout)
|
||||
}
|
||||
|
||||
return workoutsDto, nil
|
||||
}
|
||||
|
||||
func renderDate(date time.Time) string {
|
||||
return date.Format("2006-01-02")
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><path fill="currentColor" d="M165.906 18.688C15.593 59.28-42.187 198.55 92.72 245.375h-1.095c.635.086 1.274.186 1.906.28c8.985 3.077 18.83 5.733 29.532 7.94C173.36 273.35 209.74 321.22 212.69 368c-33.514 23.096-59.47 62.844-59.47 62.844l26.28 38.686L138.28 493h81.97c-40.425-40.435-11.76-85.906 36.125-85.906c48.54 0 73.945 48.112 36.156 85.906h81.126l-40.375-23.47l26.283-38.686s-26.376-40.4-60.282-63.406c3.204-46.602 39.5-94.167 89.595-113.844c10.706-2.207 20.546-4.86 29.53-7.938c.633-.095 1.273-.195 1.908-.28h-1.125c134.927-46.82 77.163-186.094-73.157-226.69c-40.722 39.37 6.54 101.683 43.626 56.877c36.9 69.08 8.603 127.587-72.28 83.406c-11.88 24.492-34.213 41.374-60.688 41.374c-26.703 0-49.168-17.167-60.97-42c-81.774 45.38-110.512-13.372-73.437-82.78c37.09 44.805 84.35-17.508 43.626-56.876zm90.79 35.92c-27.388 0-51.33 27.556-51.33 63.61c0 36.056 23.942 62.995 51.33 62.995s51.327-26.94 51.327-62.994c0-36.058-23.94-63.61-51.328-63.61z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 128 128"><path d="M93.46 39.45c6.71-1.49 15.45-8.15 16.78-11.43.78-1.92-3.11-4.92-4.15-6.13-2.38-2.76-1.42-4.12-.5-7.41 1.05-3.74-1.44-7.87-4.97-9.49s-7.75-1.11-11.3.47-6.58 4.12-9.55 6.62c-2.17-1.37-5.63-7.42-11.23-3.49-3.87 2.71-4.22 8.61-3.72 13.32 1.17 10.87 3.85 16.51 8.9 18.03 6.38 1.92 13.44.91 19.74-.49" style="fill:#ffca28"/><path d="M104.36 8.18c-.85 14.65-15.14 24.37-21.92 28.65l4.4 3.78s2.79.06 6.61-1.16c6.55-2.08 16.12-7.96 16.78-11.43.97-5.05-4.21-3.95-5.38-7.94-.61-2.11 2.97-6.1-.49-11.9M79.78 12.09s-2.55-2.61-4.44-3.8c-.94 1.77-1.61 3.69-1.94 5.67-.59 3.48 0 8.42 1.39 12.1.22.57 1.04.48 1.13-.12 1.2-7.91 3.86-13.85 3.86-13.85" style="fill:#e2a610"/><path d="M61.96 38.16S30.77 41.53 16.7 68.61s-2.11 43.5 10.55 49.48 44.56 8.09 65.31 3.17 25.94-15.12 24.97-24.97c-1.41-14.38-14.77-23.22-14.77-23.22s.53-17.76-13.25-29.29c-12.23-10.24-27.55-5.62-27.55-5.62" style="fill:#ffca28"/><path d="M74.76 83.73c-6.69-8.44-14.59-9.57-17.12-12.6-1.38-1.65-2.19-3.32-1.88-5.39.33-2.2 2.88-3.72 4.86-4.09 2.31-.44 7.82-.21 12.45 4.2 1.1 1.04.7 2.66.67 4.11-.08 3.11 4.37 6.13 7.97 3.53 3.61-2.61.84-8.42-1.49-11.24-1.76-2.13-8.14-6.82-16.07-7.56-2.23-.21-11.2-1.54-16.38 8.31-1.49 2.83-2.04 9.67 5.76 15.45 1.63 1.21 10.09 5.51 12.44 8.3 4.07 4.83 1.28 9.08-1.9 9.64-8.67 1.52-13.58-3.17-14.49-5.74-.65-1.83.03-3.81-.81-5.53-.86-1.77-2.62-2.47-4.48-1.88-6.1 1.94-4.16 8.61-1.46 12.28 2.89 3.93 6.44 6.3 10.43 7.6 14.89 4.85 22.05-2.81 23.3-8.42.92-4.11.82-7.67-1.8-10.97" style="fill:#6b4b46"/><path d="M71.16 48.99c-12.67 27.06-14.85 61.23-14.85 61.23" style="fill:none;stroke:#6b4b46;stroke-width:5;stroke-miterlimit:10"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6d4c41"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6b4b46"/><path d="M96.49 58.86c1.06-.73 4.62.53 5.62 7.5.49 3.41.64 6.71.64 6.71s-4.2-3.77-5.59-6.42c-1.75-3.35-2.43-6.59-.67-7.79" style="fill:#e2a610"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.1 KiB |
1
template/account/default.go
Normal file
1
template/account/default.go
Normal file
@@ -0,0 +1 @@
|
||||
package account
|
||||
7
template/account/workout.templ
Normal file
7
template/account/workout.templ
Normal file
@@ -0,0 +1,7 @@
|
||||
package account
|
||||
|
||||
import "spend-sparrow/types"
|
||||
|
||||
templ AccountListComp(accounts []*types.Account) {
|
||||
<main class="mx-2"></main>
|
||||
}
|
||||
@@ -3,14 +3,7 @@ package template
|
||||
templ Index() {
|
||||
<div class="h-full">
|
||||
<div class="text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
|
||||
<p class="py-6">
|
||||
Ever wanted to track your workouts and see your progress over time? spend-sparrow is the perfect
|
||||
solution for you.
|
||||
</p>
|
||||
<a href="/workout" class="">Get Started</a>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold">Spend Sparrow - Your personal finance</h1>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ templ Layout(slot templ.Component, user templ.Component) {
|
||||
<div class="h-screen flex flex-col">
|
||||
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow-sm">
|
||||
<a href="/" class="flex-1 flex gap-2">
|
||||
<img src="/static/favicon.svg" alt="spend-sparrow logo"/>
|
||||
<span>spend-sparrow</span>
|
||||
<img class="w-6" src="/static/favicon.svg" alt="Spend Sparrow logo"/>
|
||||
<span class="text-xl font-bold">SpendSparrow</span>
|
||||
</a>
|
||||
@user
|
||||
</div>
|
||||
|
||||
572
test.log
Normal file
572
test.log
Normal file
@@ -0,0 +1,572 @@
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1025"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1034"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1033"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1029"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1027"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1028"
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1032"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1026"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1030"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server...
|
||||
2025/05/04 16:09:05 INFO BASE_URL is "http://localhost:1031"
|
||||
2025/05/04 16:09:05 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1034"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1029"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1028"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1025"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1033"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1030"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1027"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1031"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1026"
|
||||
2025/05/04 16:09:05 INFO Starting server on ":1032"
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: C7CpPA1q5HSqATguH4ITVcaREZPMp/j78y0ZXp2goj4=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: GLqqnLTym8scPyB4V8fHjpHUTQBUHJrIoVO/byzly0Y=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: gkCWUdyhAU3+m6mCGRjHMYtmvRVKHoZRXUlsOM2kBFI=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: /p9htltva1j9WlwdthWyhzMKc99iYgmuqgVwV6S+qp0=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: KfwxtvpSqFx+YcwXdh+s3zBuDlBRY80/Q+qJY4YkCvI=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: KXkQn/K0GLGF2fXibq+ydR50qBNFAEj9lApqcoFKkJM=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: 96IXin1xRasBmgmzmfQ5qAeHIHTf8G5g/DLFKPkiObE=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: yORuw2suMVJCtOu7ctwTtQ+Yuc9KfiOxZFnCO17AMOQ=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: 3znobv1bV4TArb+HlZAnQud2oDr91djlmj1oowICPgw=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: DHc4LdLcXut8tz34/orrAwqqIkvDdGHUJZHW8AotxD4=
|
||||
2025/05/04 16:09:06 INFO [::1]:43956 200 GET / 367.317µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: k9OMRAttUZAW3GID+8pTAtlvNg/rhg2tCtuAXRQsK8k=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: MMHaZEyGlT00wJm8QJ/zjbeYXe5dYeRlOHSkaFrLefY=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: MT/+XjYby0V6+Ogiu0oK1CVj97doM9NN6qQJ3UI0BEU=
|
||||
2025/05/04 16:09:06 INFO [::1]:59616 200 GET / 243.011µs
|
||||
2025/05/04 16:09:06 INFO [::1]:34368 200 GET / 297.074µs
|
||||
2025/05/04 16:09:06 INFO [::1]:41530 200 GET / 631.609µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: XLhEELc4GagnjsqM9dCnsGBYUJTTalfGQkyCnLAvQ8o=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: b+t2fEsEzLlAAghoMa5ewjzEveTEIS+sxBMG6okX7ZA=
|
||||
2025/05/04 16:09:06 INFO [::1]:45866 200 GET / 187.947µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: 1VDgPWHIHVpHP7jAj4qGhJ7z/jzSzzF/1nSKKkAE/9o=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: +PX5YUQsBCxJ+ZJwysknZGsBEGowKm1u8MVq3Vqa6Us=
|
||||
2025/05/04 16:09:06 INFO [::1]:45206 200 GET / 257.289µs
|
||||
2025/05/04 16:09:06 INFO [::1]:58582 200 GET / 251.327µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: Lbi+NGRvTGirVxRtrJtG9eIbJ2kau6g/lqYlWM+g7hg=
|
||||
2025/05/04 16:09:06 INFO [::1]:54694 200 GET / 371.716µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: HQj39ldO/6RZ1O5QXjxo9l5h4eKArXR5ItB2L8vOdvw=
|
||||
2025/05/04 16:09:06 INFO [::1]:32870 200 GET / 342.581µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: YX0jsNaJrkydun3SGCNCPfQv3kKamjihqUuYdvlUKVI=
|
||||
2025/05/04 16:09:06 INFO [::1]:39118 200 GET / 382.175µs
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: Oz1R4FzF5KXvoh+UkLYKzo26g0TSRzQ8Qpr4S6GeZs0=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: g89TfWKMRSB/KYc6hgbLzaNUDE7gVNu93y0hwQ9t0ng=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: 1jX9ZQ3qF7S3XmE5qzzONuvZZW7S0QFmGKFG9wUsZys=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: zXrh7szBHAU5cay8Y5G+MDRlC0WDFEdkc252lPwgFFQ=
|
||||
2025/05/04 16:09:06 INFO [::1]:32886 200 GET / 233.733µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: 2PZFlciuS9IXsGHE/tCC4ln2WF7ioEYWYwAqVGi3MVE=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: 39QaZTEA2+VCSd2UlDZHU79PoIah0Ah6IgC5JIhDA7E=
|
||||
2025/05/04 16:09:06 INFO [::1]:39122 200 GET /static/favicon.svg 259.503µs
|
||||
2025/05/04 16:09:06 INFO [::1]:59620 200 GET / 247.07µs
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1027
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1026
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1025
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: FfjlcLEoLOpEigTj/DAkrROCKw3GmDO6iweXarlQWk4=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: w/vDIvYUaHW/MC44I+iosJEUbfutbcPDIHbvoZKjm8A=
|
||||
2025/05/04 16:09:06 INFO [::1]:43966 200 GET /auth/signin 339.434µs
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: heG8kfN+gpvo86Kw7iqm7I+FPa9BEOJbfTXvGmwqnQM=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: nesB1mHXOhBKfz2tDkMDplmQ7g2DCJAe8a6qk0KcaNU=
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: mxAV7nBg8MYYSv9rkw7+sf/dvO2L4knqZojnRv47YOI=
|
||||
2025/05/04 16:09:06 INFO Token 'invalid-csrf-token' not found
|
||||
2025/05/04 16:09:06 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:06 INFO [::1]:34374 400 POST /api/auth/signin 143.582µs
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: 0v14+y4mILt1rhGCX/ZP675Wub7XmCsxn0xQz7A4guI=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: Ofm7h/1cpZXSCiHmUVMkN7RGS4qT2OO9VDmNfW3cfg4=
|
||||
2025/05/04 16:09:06 INFO [::1]:54706 200 GET /auth/signin 151.548µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: OfaE1yY75jbkHj4SQxYdpxYQuXdxG3xyaMhPH8JwIZk=
|
||||
2025/05/04 16:09:06 INFO [::1]:41540 200 GET /auth/signin 163.099µs
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1034
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: ihlV3z7w1lTnFTuY2oZbPPZdw/XxRq1erl6wdfoJ4os=
|
||||
2025/05/04 16:09:06 INFO [::1]:45874 200 GET /auth/signin 442.52µs
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: C3zFi5om0T8kdqGPRhWr54XoLvX+AUzFKPM+7rX4ciQ=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: MnHKHrS5TqoEd0lWFLTJBRjXfLhagXUum+6K1RIj+0k=
|
||||
2025/05/04 16:09:06 INFO [::1]:58598 200 GET /auth/signin 142.921µs
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: swEu7fAiz6NC9x6bRoEBg7Z2uGKpmsMwx/mn286wxrE=
|
||||
2025/05/04 16:09:06 INFO [::1]:45208 303 GET /auth/signin 342.33µs
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1029
|
||||
2025/05/04 16:09:06 INFO [::1]:43966 401 POST /api/auth/signin 250.291568ms
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1032
|
||||
2025/05/04 16:09:06 INFO [::1]:41540 303 POST /api/auth/signin 251.921223ms
|
||||
2025/05/04 16:09:06 INFO [::1]:54706 401 POST /api/auth/signin 251.965506ms
|
||||
2025/05/04 16:09:06 INFO [::1]:58598 200 POST /api/auth/signin 250.279857ms
|
||||
2025/05/04 16:09:06 INFO [::1]:45874 200 POST /api/auth/signin 251.907547ms
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1031
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1028
|
||||
2025/05/04 16:09:06 INFO Gracefully stopped http server on :1033
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: +LBaMyqxxiZ40ESawYlRHgrdsnODTBckcx1OeSAWWwo=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: uNQ2j9KYIdO3mWCTMqvHS713erezZvpjuQwPQ4YKfso=
|
||||
2025/05/04 16:09:06 INFO [::1]:45874 200 GET /auth/signin 395.09µs
|
||||
2025/05/04 16:09:06 INFO [::1]:45874 401 POST /api/auth/signin 250.272021ms
|
||||
2025/05/04 16:09:06 INFO Anonymous session created: qeonVn4lJ5XrSzP738w10/EeiKmla7tN7I4QQxUM5Uc=
|
||||
2025/05/04 16:09:06 INFO CSRF-Token created: YlroafoG+2gUjoxq0xlxF795ZFjlgYePQauB2ZvpZIk=
|
||||
2025/05/04 16:09:06 INFO [::1]:45874 200 GET /auth/signin 237.571µs
|
||||
2025/05/04 16:09:07 INFO [::1]:45874 401 POST /api/auth/signin 250.91927ms
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1030
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1035"
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1038"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1039"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1036"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1037"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1039"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1035"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1036"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1038"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1037"
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: xhq2IBLbAz+f/CY7UOIMov+zJb3Co14in/B/kB8bV7E=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: 71c61oHlq2zOWvnUy1bf4NdAJjXDyKrD8QV2xaP6Z7Q=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: FJAnOEJqCvcxvXZ+p+0x1QefhsKiOycb1m6LwYK4qxs=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: PdNi9YruTOFlvQl4jWxyUSLu/QC+hubl6ercCqGDBAc=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: 3uqSRAb6IGH+yiUeP+lJgz3Zud1ixBB4IA8N47b8dhI=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: Ebe1moB4HEBtxEOlTI3qKEIUrtOy8lV8pHiPpix1vRM=
|
||||
2025/05/04 16:09:07 INFO [::1]:34596 200 GET / 197.385µs
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: nkMHsmtBWVoBKfwfAkkl5bqR8x7jMpnmiSJNY4QUQRo=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: RM0p9rPPjLo+80oS90RLGNU9PrmwX3A6zX29vmCPts8=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: ck31atN4NsBZYZP2r6zZUFfNdmiS9HGTZv/W+HfzR0I=
|
||||
2025/05/04 16:09:07 INFO [::1]:47544 200 GET / 267.929µs
|
||||
2025/05/04 16:09:07 INFO [::1]:54172 200 GET / 344.093µs
|
||||
2025/05/04 16:09:07 INFO [::1]:37332 200 GET / 331.66µs
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: PeH4DG/WYHSggyCRrEa0h0OUcpvNBT6oslEcHYYB6rg=
|
||||
2025/05/04 16:09:07 INFO [::1]:35134 200 GET / 345.566µs
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: RUrlOobLzrlkVlhikL2bxNzICH+nDGUlmKrapuDZtCU=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: 1EyxHttARbGmQ4mKNrhnWhlZPoGasRHZxdsA9pGFei4=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: RX/UuisLp1bsW7BdsonpHEs1H2dNx7VHqThIP9IWS7U=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: VZCcq+YZ1HR8mznXqgM5hLr7kNSdkXh7cBpnncIhO/0=
|
||||
2025/05/04 16:09:07 INFO [::1]:34610 200 GET /auth/signup 304.768µs
|
||||
2025/05/04 16:09:07 INFO Token 'invalid-csrf-token' not found
|
||||
2025/05/04 16:09:07 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:07 INFO [::1]:37344 400 POST /api/auth/signin 131.931µs
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: J2/qBrloOVEKXYutfuO2GqlK+D7GDOt+TbMl0NAW2O0=
|
||||
2025/05/04 16:09:07 INFO [::1]:35144 200 GET /auth/signup 184.079µs
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1038
|
||||
2025/05/04 16:09:07 INFO Signing up mail@mail.de
|
||||
2025/05/04 16:09:07 INFO Signing up mail@mail.de
|
||||
2025/05/04 16:09:07 INFO Sending verification email to mail@mail.de
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: n0QDZgtZbdxuLLe9b41sr++cp+Wm5O6iA8W6qzNe0iE=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: vpoo1zdtO294QjQg/8RAu74OTTgOMz6hDq0XMvPlhTU=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: 59jPp8DxfM4afaLMVVlXGTSydDm79ehsBJfrpKOA1iY=
|
||||
2025/05/04 16:09:07 INFO [::1]:54178 303 GET /auth/signin 140.818µs
|
||||
2025/05/04 16:09:07 INFO [::1]:47554 200 GET /auth/signup 167.969µs
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1035
|
||||
2025/05/04 16:09:07 INFO Signing up mail@mail.de
|
||||
2025/05/04 16:09:07 INFO [::1]:34610 200 POST /api/auth/signup 250.628749ms
|
||||
2025/05/04 16:09:07 INFO [::1]:35144 400 POST /api/auth/signup 250.680327ms
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1036
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1039
|
||||
2025/05/04 16:09:07 INFO [::1]:47554 200 POST /api/auth/signup 250.575518ms
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1037
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1042"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1040"
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1041"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1042"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1040"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1041"
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: jo6Z7JapKy5+Mp4L6uOK/FM7gTZwREXXvgMmJsSofJs=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: hCDPoztBLkErAxojn3VzQtHPjyEcgnpgvfZFESIHptI=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: U2pccLWqFqRHFPcuLd7uhUAvNQJ846mdDAvDsqA9Ahk=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: IQMsxPprvraH+EKAcCJp7bukSTQ2N7po4gtCczKvtYg=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: vvbuIZBXqU1qT8r1C/nFqwTiMSRd26vlaMzOd2S49Zs=
|
||||
2025/05/04 16:09:07 INFO [::1]:48190 200 GET / 391.504µs
|
||||
2025/05/04 16:09:07 INFO [::1]:55320 200 GET / 325.417µs
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: zpH6K3bG3YfLFT4QiLrH3shUEXYaotdyeJ+wBo2PJ+0=
|
||||
2025/05/04 16:09:07 INFO [::1]:45470 200 GET / 486.995µs
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: IxR1A3yIeNZVqvUpZDpX0wVh5zXOGe20xfzDDggECIY=
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: u+aqhieHGjYRdG3X8FuG9ijqZIyp53E7uchETvPHeqU=
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: WG9JuKkMyqod4Yj39Tw8F3+8i0c7pnGfJRBM7ppTFFc=
|
||||
2025/05/04 16:09:07 INFO [::1]:48200 200 GET /auth/verify-email 149.474µs
|
||||
2025/05/04 16:09:07 ERROR Could not get token: sql: Scan error on column index 1, name "session_id": converting NULL to string is unsupported
|
||||
2025/05/04 16:09:07 INFO Anonymous session created: QWcreHbpTncV4QAPi5uz9/3QwcolCREP5gaMcsH/97A=
|
||||
2025/05/04 16:09:07 INFO Token 'invalid-token' not found
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: YNISy37y2IHKyVR70PGM2W3YgkuS6vdEOgsXMdDmFVc=
|
||||
2025/05/04 16:09:07 INFO [::1]:55322 400 GET /auth/verify-email 111.241µs
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1041
|
||||
2025/05/04 16:09:07 INFO CSRF-Token created: 6chtzYbxsJA5B5F0UiqFPObZcHRv1WLUqiHzv8uCggQ=
|
||||
2025/05/04 16:09:07 INFO [::1]:45476 400 GET /auth/verify-email 275.704µs
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1040
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1043"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Gracefully stopped http server on :1042
|
||||
2025/05/04 16:09:07 INFO Starting server...
|
||||
2025/05/04 16:09:07 INFO BASE_URL is "http://localhost:1044"
|
||||
2025/05/04 16:09:07 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1043"
|
||||
2025/05/04 16:09:07 INFO Starting server on ":1044"
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: 854gAsAl3mE0zV1Q1svNDSFWQ6nluq3f55epIfLMhPo=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: mhaSgZjdqkhC6Sa3LuEyKchemWZXYcQBqh86Zgfd3O4=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: xLIJE9FfBg3Zgf1CUUsrX12r/CWfOc+5hvXy8ANi3Q8=
|
||||
2025/05/04 16:09:08 INFO [::1]:60358 200 GET / 316.1µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 32N6JsF7BvBZ+WoDvyS7BzkKBqZOgjOR+u9zK581yKs=
|
||||
2025/05/04 16:09:08 INFO [::1]:36078 200 GET / 417.282µs
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: EiHwa5bmN142PTYFJQdsyAfPzd7V26SgNRjLO2qGRls=
|
||||
2025/05/04 16:09:08 INFO Token 'invalid-csrf-token' not found
|
||||
2025/05/04 16:09:08 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:08 INFO [::1]:36086 400 POST /api/auth/sign-out 163.04µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1043
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: Nfk4NBMIA8O5lG5Agy+6SzwPB4Ui8/NcTokJx+ugcF8=
|
||||
2025/05/04 16:09:08 INFO [::1]:60362 200 GET / 259.192µs
|
||||
2025/05/04 16:09:08 INFO [::1]:60372 303 POST /api/auth/signout 69.102µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1044
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1045"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1048"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1047"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1049"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1046"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1045"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1048"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1046"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1047"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1049"
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: r1l6P81B5bjvAtYgEs+kpDCss1jzJSDOC6D0gPcRC1Q=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: T8aTRMQ+OAHTI0ts0FbGMHdACkPOdZl7+ELtTq3tz6g=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: 0Yd55Rluy8MSjEPfWd3a/UQQ7xsxxV3HZrs1OZE5i8g=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: W/1hRY9hi0R7kMYQXLSCi84JrPVt5a1kX3LBrITCQds=
|
||||
2025/05/04 16:09:08 INFO [::1]:34846 200 GET / 194.78µs
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: XT52HDsdTpCuo1f6ouCxb0rathiNkURS2gnsqE18DRk=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: t7RB/YcpJpwiXoVVycJfzP2BhQ55Qc0YG6zgpFQZP8E=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 4grA+kACaEyZR98Il3ElKJBimZp19xn1A27YKChpIhE=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: pb7IOSCtrmj+7M2n6bBUSVrKuFtElh4Oc7tSRBfEHYs=
|
||||
2025/05/04 16:09:08 INFO [::1]:60418 200 GET / 305.169µs
|
||||
2025/05/04 16:09:08 INFO [::1]:53504 200 GET / 344.705µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: G+TunCuuTHlb0S7VwMHiXPIikwG8NDYUTt/ZoocMPII=
|
||||
2025/05/04 16:09:08 INFO [::1]:56720 200 GET / 809.687µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: Xcaewg4EtCBsDX++vDCRBye6bNBwCxkdF7nFNP88h6U=
|
||||
2025/05/04 16:09:08 INFO [::1]:57866 200 GET / 1.212432ms
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: hZEtjYJgNMWXtU5fIBk2UHyL57zMOtHDp7znqK6LW4o=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: VWKij/LgmDomxPOq7RwXfadZP10CY6+Eq5Q9MQYgV7U=
|
||||
2025/05/04 16:09:08 INFO [::1]:56730 400 POST /api/auth/delete-account 113.726µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 3fJ2mo7ZLuRM74AdLOO3COIhtJwAqFvuSso4B0cPaNA=
|
||||
2025/05/04 16:09:08 INFO [::1]:60434 303 GET /auth/delete-account 156.708µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1045
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1048
|
||||
2025/05/04 16:09:08 INFO Token 'wrong-csrf-token' not found
|
||||
2025/05/04 16:09:08 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:08 INFO [::1]:34848 400 POST /api/auth/delete-account 116.441µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1047
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: CXDb/h5hm5uSxkbNAGKCHEmmUOVO4AMOUwsjApGZgQo=
|
||||
2025/05/04 16:09:08 INFO [::1]:53512 200 GET /auth/delete-account 180.412µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: kTJK4VhAk17nGB7nN6RZ+z5qJ1hNhDZ0y6nsKRk1zcA=
|
||||
2025/05/04 16:09:08 INFO [::1]:57868 200 GET /auth/delete-account 300.711µs
|
||||
2025/05/04 16:09:08 INFO [::1]:57868 400 POST /api/auth/delete-account 31.520081ms
|
||||
2025/05/04 16:09:08 ERROR Could not delete workouts: no such table: workout
|
||||
2025/05/04 16:09:08 INFO [::1]:53512 500 POST /api/auth/delete-account 48.218363ms
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1049
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1046
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1053"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1050"
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1052"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1051"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1055"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1054"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1053"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1055"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1054"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1052"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1051"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1050"
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: xd1mXTWn5ZZAYnTiIszwXAeUFy/fL99MeP7LwqIgerk=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: ENqUYxguF8j9Q7GLT3oWY/++eb9CUDILQ8xVSHPwVu8=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: t6vUSQA4ec5tXpIiIegzATp8DQY9toEF/KxsM57uX1s=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: 5RBBXq3M1PAtwUWqnQ7Ll5p/1gZsv00F5NKCgjuUTwU=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: PzpOOdkCCxy7rIdhrOMgWid9r49r3Dz4gFifT5g41fg=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: PY8YUKM82L/tAUjhrn53fLqD4qzFRIAEmNiHQ4x55Xk=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: yqhCY3jkfFD2/QKnRFokA+PFpmzyGKYIPwj/egC4CLE=
|
||||
2025/05/04 16:09:08 INFO [::1]:43092 200 GET / 254.533µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 77cVIkgGijSk5xB+h6wIj53NmopBuOR0rWdmF7HsI5E=
|
||||
2025/05/04 16:09:08 INFO [::1]:37470 200 GET / 279.751µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: HC5uypZg/hQkkHVjxEVtk2KKLNgeJngfpZcjHBl2tVI=
|
||||
2025/05/04 16:09:08 INFO [::1]:59020 200 GET / 276.956µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 4IheFhABdSeQmeDGZkrDb1F7rmU0/I/ff6Wz6rOBdB0=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 2b/SyQIGJWKhNZAOEs58y3NjHeNWmPX+wbmYezcq7Lo=
|
||||
2025/05/04 16:09:08 INFO [::1]:53478 200 GET / 358.06µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: OGRXde04GcOpUR58XgH7NTvvjtNXYKZiYtEFpYlpCPs=
|
||||
2025/05/04 16:09:08 INFO [::1]:46564 200 GET / 364.843µs
|
||||
2025/05/04 16:09:08 INFO [::1]:34404 200 GET / 299.659µs
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: HxvqtFVnh4XNpPNSlixBKM9CUAsZKQkp8QwKYJAsGxA=
|
||||
2025/05/04 16:09:08 INFO Anonymous session created: 4PswFHiRAHiV4zhdz1RgAZknQFbrSIUSrO9xHxfr1fM=
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: e12W/Efzfg0+QC6Qq1KDitQfLNbBu3zTeK/Bw4EzlWE=
|
||||
2025/05/04 16:09:08 INFO [::1]:34408 303 GET /auth/change-password 382.165µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 6KZmcA1f/rjcKkND/PtP94+U5sXjKSTaWD1sorBP8NI=
|
||||
2025/05/04 16:09:08 INFO [::1]:37478 200 GET /auth/signin 234.806µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1050
|
||||
2025/05/04 16:09:08 INFO [::1]:37478 401 POST /api/auth/change-password 94.389µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1052
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: 6cmY2Ik6wo7QZEtGXhg9wK5O2OEDTY8BZAA2ZFtWu54=
|
||||
2025/05/04 16:09:08 INFO [::1]:53486 200 GET /auth/change-password 228.724µs
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: O1/1VPy9ED1Krk9BiZVZqBa7MSvdZyS+LmSJq15mjVg=
|
||||
2025/05/04 16:09:08 INFO [::1]:46570 200 GET /auth/change-password 117.974µs
|
||||
2025/05/04 16:09:08 INFO Token 'invalid-csrf-token' not found
|
||||
2025/05/04 16:09:08 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:08 INFO [::1]:59030 400 POST /api/auth/change-password 156.557µs
|
||||
2025/05/04 16:09:08 INFO [::1]:46570 400 POST /api/auth/change-password 46.218µs
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1053
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1054
|
||||
2025/05/04 16:09:08 INFO CSRF-Token created: A/i/s6H3Son87BW4kq1YaXGhICDGwHxFrwgWtB7aMkM=
|
||||
2025/05/04 16:09:08 INFO [::1]:43104 200 GET /auth/change-password 195.411µs
|
||||
2025/05/04 16:09:08 INFO [::1]:53486 400 POST /api/auth/change-password 47.082787ms
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1051
|
||||
2025/05/04 16:09:08 INFO [::1]:43104 200 POST /api/auth/change-password 87.731017ms
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1057"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1056"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Gracefully stopped http server on :1055
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO Starting server...
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1058"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO BASE_URL is "http://localhost:1059"
|
||||
2025/05/04 16:09:08 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1057"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1059"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1056"
|
||||
2025/05/04 16:09:08 INFO Starting server on ":1058"
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: Lfa65ZLijd9A8ybwL7mOa+dUNnzmehnP29h7EkiMzg8=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: hPcKg1TM5o62yX3heszCWqBFUFRN3NPGq6+t+PByiH0=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: r+Js1J/2Qi1xGL2R20g+q0KDbhkQGfpvTR41Q+ubCwo=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: K6KSVynVZ2vroau1Hmq+f+upnRrtmdlak69iKe7gJnk=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: /BQo8DCsout5rcBpxHXfv4JtJv8tccA0vjTkdrhAW9s=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: 9KZlDLpU5EFWjoU0wWaR+y1kTZFRlUECmSAIqvyicnE=
|
||||
2025/05/04 16:09:09 INFO [::1]:44734 200 GET / 291.343µs
|
||||
2025/05/04 16:09:09 INFO [::1]:52430 200 GET / 324.697µs
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: ipTmbEPHNtUunc5peCg02yXJWDU6C4cIwaoKTykpLg4=
|
||||
2025/05/04 16:09:09 INFO [::1]:59864 200 GET / 237.982µs
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: QuPLy+EgiucAgHXv/hsQdrJYe7qA6z4Ul4zaQRQ+KFM=
|
||||
2025/05/04 16:09:09 INFO [::1]:51392 200 GET / 291.033µs
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: 9HjVjEW2GD1Pb5vNtA7cCn//W7hZdP1JRTMq0rb2qhA=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: QN55nsXVkitPu2PQFBROtdjSRrpjQXRMlvyUktwKbeg=
|
||||
2025/05/04 16:09:09 INFO [::1]:51398 200 GET /auth/forgot-password 686.202µs
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: W/4UsFOsg0HQgVUzBnClrpZ9hagqGujeohVDF9CptL4=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: o46grNIHFVAhOz47erqEMWMQ9Vx5zEZTLQQJT/L5bxA=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: ykibTAavOkHX+fegECTcu1NcEa3bdouuPuCy1Zk1eY4=
|
||||
2025/05/04 16:09:09 INFO [::1]:59878 200 GET /auth/forgot-password 258.801µs
|
||||
2025/05/04 16:09:09 INFO [::1]:44736 303 GET /auth/forgot-password 113.645µs
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1057
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: 5gezmcCiFCeF4ldmHYuvTsdF3/Yhoho2O+siwEBhx9E=
|
||||
2025/05/04 16:09:09 INFO Token 'invalid-csrf-token' not found
|
||||
2025/05/04 16:09:09 INFO CSRF-Token not correct
|
||||
2025/05/04 16:09:09 INFO [::1]:59884 400 POST /api/auth/forgot-password 62.308µs
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: sB7LgNGEbOF5DSSMzwmSshATc51GFcbDS1Rlv7fZJ6s=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: W4MNydFZuSdvtZIMMvUqAHUJ0WhBr23Dcoz90LKCID4=
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1058
|
||||
2025/05/04 16:09:09 INFO [::1]:52442 200 GET /auth/forgot-password 179.26µs
|
||||
2025/05/04 16:09:09 INFO [::1]:51398 200 POST /api/auth/forgot-password 251.419781ms
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1059
|
||||
2025/05/04 16:09:09 INFO [::1]:52442 200 POST /api/auth/forgot-password 250.816416ms
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1056
|
||||
2025/05/04 16:09:09 INFO Starting server...
|
||||
2025/05/04 16:09:09 INFO BASE_URL is "http://localhost:1060"
|
||||
2025/05/04 16:09:09 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:09 INFO Starting server...
|
||||
2025/05/04 16:09:09 INFO BASE_URL is "http://localhost:1061"
|
||||
2025/05/04 16:09:09 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:09 INFO Starting server...
|
||||
2025/05/04 16:09:09 INFO BASE_URL is "http://localhost:1062"
|
||||
2025/05/04 16:09:09 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:09 INFO Starting server...
|
||||
2025/05/04 16:09:09 INFO BASE_URL is "http://localhost:1063"
|
||||
2025/05/04 16:09:09 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:09 INFO Starting server on ":1063"
|
||||
2025/05/04 16:09:09 INFO Starting server on ":1062"
|
||||
2025/05/04 16:09:09 INFO Starting server on ":1060"
|
||||
2025/05/04 16:09:09 INFO Starting server on ":1061"
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: oCgpL+oZhB3Qxun4suBEgG3mPIgT5hmayl60V/1RmwQ=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: MJPq32rxlQv17sLxU5SpdSB/KwbJsZ3U4w3cvsN3uC0=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: CUlQ+bggY/0A/rDAR3sptz+J3n2Bdl9Fzke2ZFj3PRs=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: YZJRCSOkI6dGoiNbXmCAg7MYGoFUOA7Lr59XcLo3YF8=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: 96U59EFvYbOqmF26RwH2Hqu3/FFTNss8S43AM/K388Q=
|
||||
2025/05/04 16:09:09 INFO [::1]:35294 200 GET / 196.252µs
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: vK29fXyz7zbk8RZaAyVOHaKkvdXp51F15wDrMK/ZePI=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: hGjTpCf8rPPFQgaifNbuevCVNKB5rCC+gGzRyTdqVNw=
|
||||
2025/05/04 16:09:09 INFO [::1]:55424 200 GET / 284.911µs
|
||||
2025/05/04 16:09:09 INFO [::1]:44574 200 GET / 311.472µs
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: 1zsSCsDcU2ZC2NEjulCbHJ6ZOI8VlW/oboKgopYwYdk=
|
||||
2025/05/04 16:09:09 INFO [::1]:50910 200 GET / 288.638µs
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: ts1l2m7Elvo8//oCix6Lr4K0OcIICSsLd7Lj588JkqM=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: bSGjupmgc72ZgO9/prg2tGlSVG7q/B5cDZILsHiFmWo=
|
||||
2025/05/04 16:09:09 INFO [::1]:55428 200 GET /auth/forgot-password 207.965µs
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: jP7c4Q6ph70c4ZeOweRvIfBfYHE1gqgJsUJuCbUP8J4=
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: 8dSyXuUzq9vw40Gl5hNiYSmJQ6i/KCTefPiY4+9u1Wc=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: +rZh42okgPd3/fF/xl4NFwvwX3MiM7zjlcGG3zM5XFM=
|
||||
2025/05/04 16:09:09 INFO [::1]:44576 200 GET /auth/forgot-password 114.216µs
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: WKPcqDP2N7aXFj387PBryeZWCPIWg6BVP8mvLsYiCn4=
|
||||
2025/05/04 16:09:09 INFO [::1]:50926 200 GET /auth/forgot-password 251.918µs
|
||||
2025/05/04 16:09:09 INFO [::1]:55428 400 POST /api/auth/forgot-password-actual 66.767µs
|
||||
2025/05/04 16:09:09 INFO Token 'invalidToken' not found
|
||||
2025/05/04 16:09:09 INFO [::1]:44576 400 POST /api/auth/forgot-password-actual 65.374µs
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1062
|
||||
2025/05/04 16:09:09 INFO [::1]:50926 400 POST /api/auth/forgot-password-actual 43.933µs
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1060
|
||||
2025/05/04 16:09:09 INFO Gracefully stopped http server on :1061
|
||||
2025/05/04 16:09:09 INFO Anonymous session created: tdGFl8JZXU3Wc1np+/XpX0/QoAe3kiLvgQm8HjrYSig=
|
||||
2025/05/04 16:09:09 INFO CSRF-Token created: aVH669YBJv1OWpp34b+Qjuz+M5HsPODfHTFov8s12nw=
|
||||
2025/05/04 16:09:09 INFO [::1]:35308 200 GET /auth/forgot-password 240.858µs
|
||||
2025/05/04 16:09:10 INFO [::1]:35308 200 POST /api/auth/forgot-password 250.688653ms
|
||||
2025/05/04 16:09:10 INFO [::1]:35308 200 POST /api/auth/forgot-password-actual 54.280642ms
|
||||
2025/05/04 16:09:10 INFO Gracefully stopped http server on :1063
|
||||
2025/05/04 16:09:10 INFO Starting server...
|
||||
2025/05/04 16:09:10 INFO BASE_URL is "http://localhost:1066"
|
||||
2025/05/04 16:09:10 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:10 INFO Starting server...
|
||||
2025/05/04 16:09:10 INFO BASE_URL is "http://localhost:1065"
|
||||
2025/05/04 16:09:10 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:10 INFO Starting server...
|
||||
2025/05/04 16:09:10 INFO BASE_URL is "http://localhost:1064"
|
||||
2025/05/04 16:09:10 INFO ENVIRONMENT is "test"
|
||||
2025/05/04 16:09:10 INFO Starting server on ":1064"
|
||||
2025/05/04 16:09:10 INFO Starting server on ":1065"
|
||||
2025/05/04 16:09:10 INFO Starting server on ":1066"
|
||||
2025/05/04 16:09:10 INFO Anonymous session created: pNnVTAfsxKbEKVbL964pbEiz0Yo97GiKfTzgMmeDXag=
|
||||
2025/05/04 16:09:10 INFO Anonymous session created: XHGSC8/S+wpUU1+y69UJ6C6giGlJI/c+rjrCACAXpUQ=
|
||||
2025/05/04 16:09:10 INFO Anonymous session created: i6xM86SMdzJbQ2iwhFxDJUoE1huvwRUXGgR8Grz2NLA=
|
||||
2025/05/04 16:09:10 INFO CSRF-Token created: LxM0GzekPJwHiFbfIhvErKx+8ifFF1mzilPRCnWRA2M=
|
||||
2025/05/04 16:09:10 INFO CSRF-Token created: ad1UAy/BwmOoVfaERQy1r41ZQ/MQVrvrmuCepW8B2SA=
|
||||
2025/05/04 16:09:10 INFO [::1]:39678 200 GET / 453.261µs
|
||||
2025/05/04 16:09:10 INFO [::1]:53820 200 GET / 459.462µs
|
||||
2025/05/04 16:09:10 INFO CSRF-Token created: aPbPi98kbLcCPMhIkKpEdjgtfdFu7nXMsa7ILjbCE7M=
|
||||
2025/05/04 16:09:10 INFO [::1]:56498 200 GET / 556.106µs
|
||||
2025/05/04 16:09:10 INFO Anonymous session created: o4vsnS+KCdznIZ8m/RnSKUcX0SCrYGdiW6M/qACB+MI=
|
||||
2025/05/04 16:09:10 INFO Anonymous session created: ge8Q/yJxdp1EHbCMn//9MM4DbUjnp/Wf+/6uOk5e0WM=
|
||||
2025/05/04 16:09:10 INFO CSRF-Token created: tdfGaFG7xiDV9qUY/8oxdmJ+ihDVT5sgeIWApwHTzlw=
|
||||
2025/05/04 16:09:10 INFO [::1]:39684 200 GET / 97.635µs
|
||||
2025/05/04 16:09:10 INFO CSRF-Token created: d91wOB18AS3RL8mj7nGwgLaGX/a6muRyd5HPr79SC7Y=
|
||||
2025/05/04 16:09:10 INFO [::1]:56514 200 GET / 160.425µs
|
||||
2025/05/04 16:09:10 INFO Anonymous session created: s1+XE+fFIBra30AMXxWn9QLyeeVTF2VNXjFfVmmLVtk=
|
||||
2025/05/04 16:09:10 INFO CSRF-Token created: jQRbdkV6aol/nsuC8exGnkqFrVQZaM4DfkaezfuHQr4=
|
||||
2025/05/04 16:09:10 INFO [::1]:53828 404 GET /workout 129.997µs
|
||||
2025/05/04 16:09:10 INFO Gracefully stopped http server on :1066
|
||||
2025/05/04 16:09:10 INFO Gracefully stopped http server on :1064
|
||||
--- FAIL: TestIntegrationAuth (4.39s)
|
||||
--- FAIL: TestIntegrationAuth/DeleteAccount (0.00s)
|
||||
--- FAIL: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data (0.34s)
|
||||
main_test.go:919:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:919
|
||||
Error: Not equal:
|
||||
expected: 200
|
||||
actual : 500
|
||||
Test: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data
|
||||
main_test.go:924:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:924
|
||||
Error: Not equal:
|
||||
expected: 0
|
||||
actual : 1
|
||||
Test: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data
|
||||
main_test.go:927:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:927
|
||||
Error: Not equal:
|
||||
expected: 0
|
||||
actual : 1
|
||||
Test: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data
|
||||
main_test.go:930:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:930
|
||||
Error: Not equal:
|
||||
expected: 0
|
||||
actual : 1
|
||||
Test: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data
|
||||
main_test.go:932:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:932
|
||||
Error: Expected nil, but got: sqlite3.Error{Code:1, ExtendedCode:1, SystemErrno:0x0, err:"no such table: workout"}
|
||||
Test: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data
|
||||
main_test.go:933:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:933
|
||||
Error: Not equal:
|
||||
expected: 0
|
||||
actual : 1
|
||||
Test: TestIntegrationAuth/DeleteAccount/should_delete_all_user_related_data
|
||||
--- FAIL: TestIntegrationAuth/Session (0.00s)
|
||||
--- FAIL: TestIntegrationAuth/Session/should_not_have_access_to_user_information_with_outdated_session (0.25s)
|
||||
main_test.go:1611:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:1611
|
||||
Error: Not equal:
|
||||
expected: 303
|
||||
actual : 404
|
||||
Test: TestIntegrationAuth/Session/should_not_have_access_to_user_information_with_outdated_session
|
||||
main_test.go:1612:
|
||||
Error Trace: /home/tiwun/source/spend-sparrow/main_test.go:1612
|
||||
Error: Not equal:
|
||||
expected: "/auth/signin"
|
||||
actual : ""
|
||||
|
||||
Diff:
|
||||
--- Expected
|
||||
+++ Actual
|
||||
@@ -1 +1 @@
|
||||
-/auth/signin
|
||||
+
|
||||
Test: TestIntegrationAuth/Session/should_not_have_access_to_user_information_with_outdated_session
|
||||
FAIL
|
||||
2025/05/04 16:09:10 INFO Gracefully stopped http server on :1065
|
||||
FAIL spend-sparrow 4.413s
|
||||
ok spend-sparrow/db (cached)
|
||||
? spend-sparrow/handler [no test files]
|
||||
? spend-sparrow/handler/middleware [no test files]
|
||||
? spend-sparrow/log [no test files]
|
||||
? spend-sparrow/mocks [no test files]
|
||||
ok spend-sparrow/service (cached)
|
||||
? spend-sparrow/template [no test files]
|
||||
? spend-sparrow/template/account [no test files]
|
||||
? spend-sparrow/template/auth [no test files]
|
||||
? spend-sparrow/template/mail [no test files]
|
||||
? spend-sparrow/template/workout [no test files]
|
||||
? spend-sparrow/types [no test files]
|
||||
? spend-sparrow/utils [no test files]
|
||||
FAIL
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
type User struct {
|
||||
Id uuid.UUID
|
||||
GroupId uuid.UUID
|
||||
Email string
|
||||
EmailVerified bool
|
||||
EmailVerifiedAt *time.Time
|
||||
|
||||
91
types/money.go
Normal file
91
types/money.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// At the center of the application is the transaction.
|
||||
//
|
||||
// Every piece of data should be calculated based on transactions. This means potential calculation errors can be fixed later in time.
|
||||
//
|
||||
// If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future. But the transaction should always be the source of truth.
|
||||
type Transaction struct {
|
||||
Id uuid.UUID
|
||||
GroupId uuid.UUID
|
||||
|
||||
AccountId uuid.UUID
|
||||
// nil indicates that the transaction is not yet associated with a piggy bank
|
||||
PiggyBankId *uuid.UUID
|
||||
|
||||
// The internal transaction is amove between e.g. an account and a piggy bank to execute a savings plan
|
||||
Internal bool
|
||||
|
||||
// The value of the transacion. Negative for outgoing and positive for incoming
|
||||
Value int64
|
||||
Timestamp time.Time
|
||||
|
||||
Note string
|
||||
|
||||
CreatedAt time.Time
|
||||
// either "<username>" or "system-<subsystem>"
|
||||
CreatedBy uuid.UUID
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy uuid.UUID
|
||||
}
|
||||
|
||||
// The Account holds money
|
||||
type Account struct {
|
||||
Id uuid.UUID
|
||||
GroupId uuid.UUID
|
||||
|
||||
// Custom Name of the account, e.g. "Bank", "Cash", "Credit Card"
|
||||
Name string
|
||||
|
||||
CurrentBalance int64
|
||||
LastTransaction *time.Time
|
||||
// The current precalculated value of:
|
||||
// Account.Balance - [PiggyBank.Balance...]
|
||||
OinkBalance int64
|
||||
|
||||
CreatedAt time.Time
|
||||
CreatedBy uuid.UUID
|
||||
UpdatedAt *time.Time
|
||||
UpdatedBy *uuid.UUID
|
||||
}
|
||||
|
||||
// The PiggyBank is a fictional account. The money it "holds" is actually in the Account
|
||||
type PiggyBank struct {
|
||||
Id uuid.UUID
|
||||
GroupId uuid.UUID
|
||||
|
||||
AccountId uuid.UUID
|
||||
Name string
|
||||
|
||||
CurrentBalance int64
|
||||
|
||||
CreatedAt time.Time
|
||||
CreatedBy uuid.UUID
|
||||
UpdatedAt *time.Time
|
||||
UpdatedBy *uuid.UUID
|
||||
}
|
||||
|
||||
// The SavingsPlan is applied every interval to the PiggyBank/Account as a transaction
|
||||
type SavingsPlan struct {
|
||||
Id uuid.UUID
|
||||
GroupId uuid.UUID
|
||||
|
||||
PiggyBankId uuid.UUID
|
||||
|
||||
MonthlySaving int64
|
||||
|
||||
ValidFrom time.Time
|
||||
/// nil means it is valid indefinitely
|
||||
ValidTo *time.Time
|
||||
|
||||
CreatedAt time.Time
|
||||
CreatedBy uuid.UUID
|
||||
UpdatedAt *time.Time
|
||||
UpdatedBy *uuid.UUID
|
||||
}
|
||||
@@ -6,4 +6,5 @@ import (
|
||||
|
||||
var (
|
||||
ErrInternal = errors.New("internal server error")
|
||||
ErrUnauthorized = errors.New("You are not authorized to perform this action.")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user