8 Commits

Author SHA1 Message Date
dc8e87ba0d feat(security): #286 fix test
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 44s
2024-12-08 23:21:29 +01:00
c9e0188b60 feat(security): #286 anonymous sign in for csrf token on login form 2024-12-08 23:19:45 +01:00
425a7cc989 feat(security): #286 first try on csrf 2024-12-08 23:19:45 +01:00
04c6d0e71d tbs 2024-12-08 23:19:45 +01:00
0170d63ae9 tbs 2024-12-08 23:19:45 +01:00
778033e494 tbs 2024-12-08 23:19:45 +01:00
fe2d7c0fd4 tbs 2024-12-08 23:19:45 +01:00
b9d50d986f tbs 2024-12-08 23:19:45 +01:00
39 changed files with 884 additions and 2011 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.4@sha256:70031844b8c225351d0bb63e2c383f80db85d92ba894e3da7e13bcf80efa9a37 AS builder_go
FROM golang:1.23.4@sha256:574185e5c6b9d09873f455a7c205ea0514bfd99738c5dc7750196403a44ed4b7 AS builder_go
WORKDIR /me-fit
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.2
RUN go install github.com/a-h/templ/cmd/templ@latest

View File

@@ -17,22 +17,89 @@ var (
ErrAlreadyExists = errors.New("row already exists")
)
type User struct {
Id uuid.UUID
Email string
EmailVerified bool
EmailVerifiedAt *time.Time
IsAdmin bool
Password []byte
Salt []byte
CreateAt time.Time
}
func NewUser(id uuid.UUID, email string, emailVerified bool, emailVerifiedAt *time.Time, isAdmin bool, password []byte, salt []byte, createAt time.Time) *User {
return &User{
Id: id,
Email: email,
EmailVerified: emailVerified,
EmailVerifiedAt: emailVerifiedAt,
IsAdmin: isAdmin,
Password: password,
Salt: salt,
CreateAt: createAt,
}
}
type Session struct {
Id string
UserId uuid.UUID
CreatedAt time.Time
ExpiresAt time.Time
}
func NewSession(id string, userId uuid.UUID, createdAt time.Time, expiresAt time.Time) *Session {
return &Session{
Id: id,
UserId: userId,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
}
}
type Token struct {
UserId uuid.UUID
SessionId string
Token string
Type TokenType
CreatedAt time.Time
ExpiresAt time.Time
}
type TokenType string
var (
TokenTypeEmailVerify TokenType = "email_verify"
TokenTypePasswordReset TokenType = "password_reset"
TokenTypeCsrf TokenType = "csrf"
)
func NewToken(userId uuid.UUID, sessionId string, token string, tokenType TokenType, createdAt time.Time, expiresAt time.Time) *Token {
return &Token{
UserId: userId,
SessionId: sessionId,
Token: token,
Type: tokenType,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
}
}
type Auth interface {
InsertUser(user *types.User) error
UpdateUser(user *types.User) error
GetUserByEmail(email string) (*types.User, error)
GetUser(userId uuid.UUID) (*types.User, error)
InsertUser(user *User) error
UpdateUser(user *User) error
GetUserByEmail(email string) (*User, error)
GetUser(userId uuid.UUID) (*User, error)
DeleteUser(userId uuid.UUID) error
InsertToken(token *types.Token) error
GetToken(token string) (*types.Token, error)
GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error)
InsertToken(token *Token) error
GetToken(token string) (*Token, error)
GetTokensByUserIdAndType(userId uuid.UUID, tokenType TokenType) ([]*Token, error)
GetTokensBySessionIdAndType(sessionId string, tokenType TokenType) ([]*Token, error)
DeleteToken(token string) error
InsertSession(session *types.Session) error
GetSession(sessionId string) (*types.Session, error)
GetSessions(userId uuid.UUID) ([]*types.Session, error)
InsertSession(session *Session) error
GetSession(sessionId string) (*Session, error)
DeleteSession(sessionId string) error
DeleteOldSessions(userId uuid.UUID) error
}
@@ -45,7 +112,7 @@ func NewAuthSqlite(db *sql.DB) *AuthSqlite {
return &AuthSqlite{db: db}
}
func (db AuthSqlite) InsertUser(user *types.User) error {
func (db AuthSqlite) InsertUser(user *User) error {
_, err := db.db.Exec(`
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -63,7 +130,7 @@ func (db AuthSqlite) InsertUser(user *types.User) error {
return nil
}
func (db AuthSqlite) UpdateUser(user *types.User) error {
func (db AuthSqlite) UpdateUser(user *User) error {
_, err := db.db.Exec(`
UPDATE user
SET email_verified = ?, email_verified_at = ?, password = ?
@@ -78,7 +145,7 @@ func (db AuthSqlite) UpdateUser(user *types.User) error {
return nil
}
func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
func (db AuthSqlite) GetUserByEmail(email string) (*User, error) {
var (
userId uuid.UUID
emailVerified bool
@@ -102,10 +169,10 @@ func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
}
}
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
return NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
}
func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
func (db AuthSqlite) GetUser(userId uuid.UUID) (*User, error) {
var (
email string
emailVerified bool
@@ -129,7 +196,7 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
}
}
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
return NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
}
func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
@@ -177,7 +244,7 @@ func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
return nil
}
func (db AuthSqlite) InsertToken(token *types.Token) error {
func (db AuthSqlite) InsertToken(token *Token) error {
_, err := db.db.Exec(`
INSERT INTO token (user_id, session_id, type, token, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt)
@@ -190,11 +257,11 @@ func (db AuthSqlite) InsertToken(token *types.Token) error {
return nil
}
func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
func (db AuthSqlite) GetToken(token string) (*Token, error) {
var (
userId uuid.UUID
sessionId string
tokenType types.TokenType
tokenType TokenType
createdAtStr string
expiresAtStr string
createdAt time.Time
@@ -228,10 +295,10 @@ func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
return nil, types.ErrInternal
}
return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
return NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
}
func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType TokenType) ([]*Token, error) {
query, err := db.db.Query(`
SELECT token, created_at, expires_at
@@ -247,7 +314,7 @@ func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.
return getTokensFromQuery(query, userId, "", tokenType)
}
func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType TokenType) ([]*Token, error) {
query, err := db.db.Query(`
SELECT token, created_at, expires_at
@@ -263,8 +330,8 @@ func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType typ
return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType)
}
func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
var tokens []*types.Token
func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType TokenType) ([]*Token, error) {
var tokens []*Token
hasRows := false
for query.Next() {
@@ -296,7 +363,7 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok
return nil, types.ErrInternal
}
tokens = append(tokens, types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt))
tokens = append(tokens, NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt))
}
if !hasRows {
@@ -315,7 +382,7 @@ func (db AuthSqlite) DeleteToken(token string) error {
return nil
}
func (db AuthSqlite) InsertSession(session *types.Session) error {
func (db AuthSqlite) InsertSession(session *Session) error {
_, err := db.db.Exec(`
INSERT INTO session (session_id, user_id, created_at, expires_at)
@@ -329,7 +396,7 @@ func (db AuthSqlite) InsertSession(session *types.Session) error {
return nil
}
func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) {
func (db AuthSqlite) GetSession(sessionId string) (*Session, error) {
var (
userId uuid.UUID
@@ -343,51 +410,15 @@ func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) {
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
if err != nil {
log.Warn("Session not found: %v", err)
return nil, ErrNotFound
}
return types.NewSession(sessionId, userId, createdAt, expiresAt), nil
}
func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*types.Session, error) {
sessions, err := db.db.Query(`
SELECT session_id, created_at, expires_at
FROM session
WHERE user_id = ?`, userId)
if err != nil {
log.Error("Could not get sessions: %v", err)
return nil, types.ErrInternal
}
var result []*types.Session
for sessions.Next() {
var (
sessionId string
createdAt time.Time
expiresAt time.Time
)
err := sessions.Scan(&sessionId, &createdAt, &expiresAt)
if err != nil {
log.Error("Could not scan session: %v", err)
return nil, types.ErrInternal
}
session := types.NewSession(sessionId, userId, createdAt, expiresAt)
result = append(result, session)
}
return result, nil
return NewSession(sessionId, userId, createdAt, expiresAt), nil
}
func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
_, err := db.db.Exec(`
DELETE FROM session
WHERE expires_at < datetime('now')
AND user_id = ?`, userId)
// Delete old inactive sessions
_, err := db.db.Exec("DELETE FROM session WHERE created_at < datetime('now','-8 hours') AND user_id = ?", userId)
if err != nil {
log.Error("Could not delete old sessions: %v", err)
return types.ErrInternal
@@ -397,6 +428,7 @@ func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
func (db AuthSqlite) DeleteSession(sessionId string) error {
if sessionId != "" {
_, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId)
if err != nil {
log.Error("Could not delete session: %v", err)

View File

@@ -38,7 +38,7 @@ func TestUser(t *testing.T) {
verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expected := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
expected := NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(expected)
assert.Nil(t, err)
@@ -68,7 +68,7 @@ func TestUser(t *testing.T) {
verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
user := NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(user)
assert.Nil(t, err)
@@ -83,7 +83,7 @@ func TestUser(t *testing.T) {
underTest := AuthSqlite{db: db}
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
user := NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
err := underTest.InsertUser(user)
assert.Equal(t, types.ErrInternal, err)
@@ -101,7 +101,7 @@ func TestToken(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expiresAt := createAt.Add(24 * time.Hour)
expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt)
expected := NewToken(uuid.New(), "sessionId", "token", TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected)
assert.Nil(t, err)
@@ -113,13 +113,13 @@ func TestToken(t *testing.T) {
expected.SessionId = ""
actuals, err := underTest.GetTokensByUserIdAndType(expected.UserId, expected.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected}, actuals)
assert.Equal(t, []*Token{expected}, actuals)
expected.SessionId = "sessionId"
expected.UserId = uuid.Nil
actuals, err = underTest.GetTokensBySessionIdAndType(expected.SessionId, expected.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected}, actuals)
assert.Equal(t, []*Token{expected}, actuals)
})
t.Run("should insert and return multiple tokens", func(t *testing.T) {
t.Parallel()
@@ -130,8 +130,8 @@ func TestToken(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expiresAt := createAt.Add(24 * time.Hour)
userId := uuid.New()
expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt)
expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt)
expected1 := NewToken(userId, "sessionId", "token1", TokenTypeCsrf, createAt, expiresAt)
expected2 := NewToken(userId, "sessionId", "token2", TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected1)
assert.Nil(t, err)
@@ -142,7 +142,7 @@ func TestToken(t *testing.T) {
expected2.UserId = uuid.Nil
actuals, err := underTest.GetTokensBySessionIdAndType(expected1.SessionId, expected1.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
assert.Equal(t, []*Token{expected1, expected2}, actuals)
expected1.SessionId = ""
expected2.SessionId = ""
@@ -150,7 +150,7 @@ func TestToken(t *testing.T) {
expected2.UserId = userId
actuals, err = underTest.GetTokensByUserIdAndType(userId, expected1.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
assert.Equal(t, []*Token{expected1, expected2}, actuals)
})
t.Run("should return ErrNotFound", func(t *testing.T) {
@@ -162,10 +162,10 @@ func TestToken(t *testing.T) {
_, err := underTest.GetToken("nonExistent")
assert.Equal(t, ErrNotFound, err)
_, err = underTest.GetTokensByUserIdAndType(uuid.New(), types.TokenTypeEmailVerify)
_, err = underTest.GetTokensByUserIdAndType(uuid.New(), TokenTypeEmailVerify)
assert.Equal(t, ErrNotFound, err)
_, err = underTest.GetTokensBySessionIdAndType("sessionId", types.TokenTypeEmailVerify)
_, err = underTest.GetTokensBySessionIdAndType("sessionId", TokenTypeEmailVerify)
assert.Equal(t, ErrNotFound, err)
})
t.Run("should return ErrAlreadyExists", func(t *testing.T) {
@@ -176,7 +176,7 @@ func TestToken(t *testing.T) {
verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
user := NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(user)
assert.Nil(t, err)
@@ -191,7 +191,7 @@ func TestToken(t *testing.T) {
underTest := AuthSqlite{db: db}
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
user := NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
err := underTest.InsertUser(user)
assert.Equal(t, types.ErrInternal, err)

4
go.mod
View File

@@ -10,8 +10,8 @@ require (
github.com/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.20.5
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.31.0
golang.org/x/net v0.33.0
golang.org/x/crypto v0.30.0
golang.org/x/net v0.29.0
)
require (

26
go.sum
View File

@@ -4,12 +4,9 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -21,19 +18,10 @@ 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.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
@@ -43,24 +31,16 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -31,8 +31,8 @@ func NewAuth(service service.Auth, render *Render) Auth {
}
func (handler AuthImpl) Handle(router *http.ServeMux) {
router.Handle("GET /auth/signin", handler.handleSignInPage())
router.Handle("POST /api/auth/signin", handler.handleSignIn())
router.Handle("/auth/signin", handler.handleSignInPage())
router.Handle("/api/auth/signin", handler.handleSignIn())
router.Handle("/auth/signup", handler.handleSignUpPage())
router.Handle("/auth/verify", handler.handleSignUpVerifyPage())
@@ -40,7 +40,7 @@ func (handler AuthImpl) Handle(router *http.ServeMux) {
router.Handle("/auth/verify-email", handler.handleSignUpVerifyResponsePage())
router.Handle("/api/auth/signup", handler.handleSignUp())
router.Handle("POST /api/auth/signout", handler.handleSignOut())
router.Handle("/api/auth/signout", handler.handleSignOut())
router.Handle("/auth/delete-account", handler.handleDeleteAccountPage())
router.Handle("/api/auth/delete-account", handler.handleDeleteAccountComp())
@@ -48,9 +48,9 @@ func (handler AuthImpl) Handle(router *http.ServeMux) {
router.Handle("/auth/change-password", handler.handleChangePasswordPage())
router.Handle("/api/auth/change-password", handler.handleChangePasswordComp())
router.Handle("/auth/forgot-password", handler.handleForgotPasswordPage())
router.Handle("/api/auth/forgot-password", handler.handleForgotPasswordComp())
router.Handle("/api/auth/forgot-password-actual", handler.handleForgotPasswordResponseComp())
router.Handle("/auth/reset-password", handler.handleResetPasswordPage())
router.Handle("/api/auth/reset-password", handler.handleForgotPasswordComp())
router.Handle("/api/auth/reset-password-actual", handler.handleForgotPasswordResponseComp())
}
var (
@@ -59,9 +59,9 @@ var (
func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user != nil {
if !user.EmailVerified {
session := middleware.GetSession(r)
if session != nil {
if !session.User.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
@@ -77,28 +77,37 @@ func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
func (handler AuthImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*service.User, error) {
var email = r.FormValue("email")
var password = r.FormValue("password")
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*types.User, error) {
session := middleware.GetSession(r)
email := r.FormValue("email")
password := r.FormValue("password")
session, user, err := handler.service.SignIn(session, email, password)
session, err := handler.service.SignIn(email, password)
if err != nil {
return nil, err
}
cookie := middleware.CreateSessionCookie(session.Id)
cookie := http.Cookie{
Name: "id",
Value: session.Id,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &cookie)
return user, nil
return session.User, nil
})
if err != nil {
if err == service.ErrInvalidCredentials {
utils.TriggerToast(w, r, "error", "Invalid email or password", http.StatusUnauthorized)
if err == service.ErrInvaidCredentials {
utils.TriggerToast(w, r, "error", "Invalid email or password")
http.Error(w, "Invalid email or password", http.StatusUnauthorized)
} else {
utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError)
log.Error("Error signing in: %v", err)
http.Error(w, "An error occurred", http.StatusInternalServerError)
}
return
}
@@ -113,10 +122,10 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
session := middleware.GetSession(r)
if user != nil {
if !user.EmailVerified {
if session != nil {
if !session.User.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
@@ -131,30 +140,31 @@ func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
if user.EmailVerified {
if session.User.EmailVerified {
utils.DoRedirect(w, r, "/")
return
}
signIn := auth.VerifyComp()
handler.render.RenderLayout(r, w, signIn, user)
handler.render.RenderLayout(r, w, signIn, session.User)
}
}
func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
user := session.User
go handler.service.SendVerificationMail(user.Id, user.Email)
_, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
@@ -171,17 +181,11 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
err := handler.service.VerifyUserEmail(token)
isVerified := err == nil
comp := auth.VerifyResponseComp(isVerified)
var status int
if isVerified {
status = http.StatusOK
if err != nil {
utils.DoRedirect(w, r, "/auth/signin")
} else {
status = http.StatusBadRequest
utils.DoRedirect(w, r, "/")
}
handler.render.RenderLayoutWithStatus(r, w, comp, nil, status)
}
}
@@ -191,32 +195,27 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc {
var password = r.FormValue("password")
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) {
log.Info("Signing up %v", email)
user, err := handler.service.SignUp(email, password)
if err != nil {
return nil, err
}
log.Info("Sending verification email to %v", user.Email)
go handler.service.SendVerificationMail(user.Id, user.Email)
return nil, nil
})
if err != nil {
if errors.Is(err, types.ErrInternal) {
utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError)
utils.TriggerToast(w, r, "error", "An error occurred")
return
} else if errors.Is(err, service.ErrInvalidEmail) {
utils.TriggerToast(w, r, "error", "The email provided is invalid", http.StatusBadRequest)
return
} else if errors.Is(err, service.ErrInvalidPassword) {
utils.TriggerToast(w, r, "error", service.ErrInvalidPassword.Error(), http.StatusBadRequest)
utils.TriggerToast(w, r, "error", "The email provided is invalid")
return
}
// If err is "service.ErrAccountExists", then just continue
// If the "service.ErrAccountExists", then just continue
}
utils.TriggerToast(w, r, "success", "An activation link has been send to your email", http.StatusOK)
utils.TriggerToast(w, r, "success", "A link to activate your account has been emailed to the address provided.")
}
}
@@ -249,34 +248,36 @@ func (handler AuthImpl) handleSignOut() http.HandlerFunc {
func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.DeleteAccountComp()
handler.render.RenderLayout(r, w, comp, user)
handler.render.RenderLayout(r, w, comp, session.User)
}
}
func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
password := r.FormValue("password")
err := handler.service.DeleteAccount(user, password)
_, err := handler.service.SignIn(session.User.Email, password)
if err != nil {
if err == service.ErrInvalidCredentials {
utils.TriggerToast(w, r, "error", "Password not correct", http.StatusBadRequest)
} else {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
}
utils.TriggerToast(w, r, "error", "Password not correct")
return
}
err = handler.service.DeleteAccount(session.User)
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
@@ -289,15 +290,15 @@ func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
isPasswordReset := r.URL.Query().Has("token")
user := middleware.GetUser(r)
session := middleware.GetSession(r)
if user == nil && !isPasswordReset {
if session == nil && !isPasswordReset {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.ChangePasswordComp(isPasswordReset)
handler.render.RenderLayout(r, w, comp, user)
handler.render.RenderLayout(r, w, comp, session.User)
}
}
@@ -305,8 +306,7 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r)
user := middleware.GetUser(r)
if session == nil || user == nil {
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
@@ -314,27 +314,27 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
currPass := r.FormValue("current-password")
newPass := r.FormValue("new-password")
err := handler.service.ChangePassword(user, session.Id, currPass, newPass)
err := handler.service.ChangePassword(session.User, currPass, newPass)
if err != nil {
utils.TriggerToast(w, r, "error", "Password not correct", http.StatusUnauthorized)
utils.TriggerToast(w, r, "error", "Password not correct")
return
}
utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK)
utils.TriggerToast(w, r, "success", "Password changed")
}
}
func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
func (handler AuthImpl) handleResetPasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user != nil {
utils.DoRedirect(w, r, "/")
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.ResetPasswordComp()
handler.render.RenderLayout(r, w, comp, user)
handler.render.RenderLayout(r, w, comp, session.User)
}
}
@@ -343,19 +343,15 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
email := r.FormValue("email")
if email == "" {
utils.TriggerToast(w, r, "error", "Please enter an email", http.StatusBadRequest)
utils.TriggerToast(w, r, "error", "Please enter an email")
return
}
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) {
err := handler.service.SendForgotPasswordMail(email)
return nil, err
})
err := handler.service.SendForgotPasswordMail(email)
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
utils.TriggerToast(w, r, "error", "Internal Server Error")
} else {
utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent", http.StatusOK)
utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent")
}
}
}
@@ -366,18 +362,23 @@ func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL"))
if err != nil {
log.Error("Could not get current URL: %v", err)
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
token := pageUrl.Query().Get("token")
if token == "" {
utils.TriggerToast(w, r, "error", "No token")
return
}
newPass := r.FormValue("new-password")
err = handler.service.ForgotPassword(token, newPass)
if err != nil {
utils.TriggerToast(w, r, "error", err.Error(), http.StatusInternalServerError)
utils.TriggerToast(w, r, "error", err.Error())
} else {
utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK)
utils.TriggerToast(w, r, "success", "Password changed")
}
}
}

View File

@@ -32,7 +32,11 @@ func (handler IndexImpl) Handle(router *http.ServeMux) {
func (handler IndexImpl) handleIndexAnd404() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
session := middleware.GetSession(r)
var user *service.User
if session != nil {
user = session.User
}
var comp templ.Component

View File

@@ -2,63 +2,40 @@ package middleware
import (
"context"
"net/http"
"me-fit/service"
"me-fit/types"
"net/http"
)
type ContextKey string
var SessionKey ContextKey = "session"
var UserKey ContextKey = "user"
func Authenticate(service service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sessionId := getSessionID(r)
session, user, _ := service.SignInSession(sessionId)
session, _ := service.SignInSession(sessionId)
var err error
// Always sign in anonymous
// This way, we can always generate csrf tokens
if session == nil {
session, err = service.SignInAnonymous()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if session != nil {
ctx := context.WithValue(r.Context(), SessionKey, session)
cookie := CreateSessionCookie(session.Id)
http.SetCookie(w, &cookie)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
ctx := r.Context()
ctx = context.WithValue(ctx, UserKey, user)
ctx = context.WithValue(ctx, SessionKey, session)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func GetUser(r *http.Request) *types.User {
obj := r.Context().Value(UserKey)
if obj == nil {
return nil
}
return obj.(*types.User)
}
func GetSession(r *http.Request) *types.Session {
func GetSession(r *http.Request) *service.Session {
obj := r.Context().Value(SessionKey)
if obj == nil {
return nil
}
return obj.(*types.Session)
return obj.(*service.Session)
}
func getSessionID(r *http.Request) string {

View File

@@ -1,23 +0,0 @@
package middleware
import (
"net/http"
"strings"
)
func CacheControl(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
cached := false
if strings.HasPrefix(path, "/static") {
cached = true
}
if !cached {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,29 @@
package middleware
import "net/http"
func ContentSecurityPolicy(next http.Handler) http.Handler {
values := map[string]string{
"default-src": "'none'",
"script-src": "'self' https://umami.me-fit.eu",
"connect-src": "'self' https://umami.me-fit.eu",
"img-src": "'self'",
"style-src": "'self'",
"form-action": "'self'",
"frame-ancestors": "'none'",
}
var headerValue string
for key, value := range values {
headerValue += key + " " + value + "; "
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// While this value can be overridden, it can't be moved to after the next.ServeHTTP call,
// because if the response writer get's closed, the headers can't be set anymore
w.Header().Set("Content-Security-Policy", headerValue)
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,13 @@
package middleware
import (
"net/http"
)
func Coop(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,13 @@
package middleware
import (
"net/http"
)
func Corp(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,23 @@
package middleware
import (
"me-fit/types"
"net/http"
)
func Cors(serverSettings *types.Settings) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -2,22 +2,20 @@ package middleware
import (
"fmt"
"net/http"
"strings"
"me-fit/log"
"me-fit/service"
"me-fit/types"
"me-fit/utils"
"net/http"
)
type csrfResponseWriter struct {
http.ResponseWriter
auth service.Auth
session *types.Session
session *service.Session
}
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *types.Session) *csrfResponseWriter {
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *service.Session) *csrfResponseWriter {
return &csrfResponseWriter{
ResponseWriter: w,
auth: auth,
@@ -27,11 +25,12 @@ func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *ty
func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
dataStr := string(data)
csrfToken, err := rr.auth.GetCsrfToken(rr.session)
if err == nil {
csrfInput := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s" />`, csrfToken)
dataStr = strings.ReplaceAll(dataStr, "</form>", csrfInput+"</form>")
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
if strings.Contains(dataStr, "</form>") {
csrfToken, err := rr.auth.GetCsrfToken(rr.session)
if err == nil {
csrfField := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s">`, csrfToken)
dataStr = strings.ReplaceAll(dataStr, "</form>", csrfField+"</form>")
}
}
return rr.ResponseWriter.Write([]byte(dataStr))
@@ -53,20 +52,31 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
r.Method == http.MethodPatch {
csrfToken := r.FormValue("csrf-token")
if csrfToken == "" {
csrfToken = r.Header.Get("csrf-token")
}
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
log.Info("CSRF-Token not correct")
if r.Header.Get("HX-Request") == "true" {
utils.TriggerToast(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
} else {
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
}
if csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
http.Error(w, "", http.StatusForbidden)
return
}
}
if session == nil {
var err error
session, err = auth.SignInAnonymous()
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
}
cookie := http.Cookie{
Name: "id",
Value: session.Id,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &cookie)
responseWriter := newCsrfResponseWriter(w, auth, session)
next.ServeHTTP(responseWriter, r)
})

View File

@@ -1,15 +0,0 @@
package middleware
import "net/http"
func CreateSessionCookie(sessionId string) http.Cookie {
return http.Cookie{
Name: "id",
Value: sessionId,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
}

View File

@@ -1,12 +1,12 @@
package middleware
import (
"me-fit/log"
"net/http"
"strconv"
"time"
"me-fit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

View File

@@ -1,40 +0,0 @@
package middleware
import (
"net/http"
"me-fit/types"
)
func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
w.Header().Set("Content-Security-Policy",
"default-src 'none'; "+
"script-src 'self' https://umami.me-fit.eu; "+
"connect-src 'self' https://umami.me-fit.eu; "+
"img-src 'self'; "+
"style-src 'self'; "+
"form-action 'self'; "+
"frame-ancestors 'none'; ",
)
w.Header().Set("Cross-Origin-Resource-Policy", "same-site") // same-site, as same origin prohibits umami
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=(), interest-cohort=()")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -2,6 +2,7 @@ package handler
import (
"me-fit/log"
"me-fit/service"
"me-fit/template"
"me-fit/template/auth"
"me-fit/types"
@@ -21,9 +22,7 @@ func NewRender(settings *types.Settings) *Render {
}
}
func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, comp templ.Component, status int) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(status)
func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.Component) {
err := comp.Render(r.Context(), w)
if err != nil {
log.Error("Failed to render layout: %v", err)
@@ -31,22 +30,14 @@ func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, c
}
}
func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.Component) {
render.RenderWithStatus(r, w, comp, http.StatusOK)
}
func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User) {
render.RenderLayoutWithStatus(r, w, slot, user, http.StatusOK)
}
func (render *Render) RenderLayoutWithStatus(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User, status int) {
func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *service.User) {
userComp := render.getUserComp(user)
layout := template.Layout(slot, userComp, render.settings.Environment)
render.RenderWithStatus(r, w, layout, status)
render.Render(r, w, layout)
}
func (render *Render) getUserComp(user *types.User) templ.Component {
func (render *Render) getUserComp(user *service.User) templ.Component {
if user != nil {
return auth.UserComp(user.Email)

View File

@@ -2,6 +2,7 @@ package handler
import (
"me-fit/handler/middleware"
"me-fit/log"
"me-fit/service"
"me-fit/template/workout"
"me-fit/utils"
@@ -38,22 +39,22 @@ func (handler WorkoutImpl) Handle(router *http.ServeMux) {
func (handler WorkoutImpl) handleWorkoutPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == 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)
handler.render.RenderLayout(r, w, comp, session.User)
}
}
func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
@@ -64,9 +65,9 @@ func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc {
var repsStr = r.FormValue("reps")
wo := service.NewWorkoutDto("", dateStr, typeStr, setsStr, repsStr)
wo, err := handler.service.AddWorkout(user, wo)
wo, err := handler.service.AddWorkout(session.User, wo)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest)
utils.TriggerToast(w, r, "error", "Invalid input values")
http.Error(w, "Invalid input values", http.StatusBadRequest)
return
}
@@ -79,13 +80,13 @@ func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc {
func (handler WorkoutImpl) handleGetWorkout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
workouts, err := handler.service.GetWorkouts(user)
workouts, err := handler.service.GetWorkouts(session.User)
if err != nil {
return
}
@@ -102,27 +103,33 @@ func (handler WorkoutImpl) handleGetWorkout() http.HandlerFunc {
func (handler WorkoutImpl) handleDeleteWorkout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
session := middleware.GetSession(r)
if session == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
rowId := r.PathValue("id")
if rowId == "" {
utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest)
http.Error(w, "Missing required fields", http.StatusBadRequest)
log.Warn("Missing required fields for workout delete")
utils.TriggerToast(w, r, "error", "Missing ID field")
return
}
rowIdInt, err := strconv.Atoi(rowId)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest)
http.Error(w, "Invalid ID", http.StatusBadRequest)
log.Warn("Invalid ID for workout delete")
utils.TriggerToast(w, r, "error", "Invalid ID")
return
}
err = handler.service.DeleteWorkout(user, rowIdInt)
err = handler.service.DeleteWorkout(session.User, rowIdInt)
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Error("Could not delete workout: %v", err.Error())
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
}

View File

@@ -128,9 +128,11 @@ func createHandler(d *sql.DB, serverSettings *types.Settings) http.Handler {
return middleware.Wrapper(
router,
middleware.Log,
middleware.CacheControl,
middleware.SecurityHeaders(serverSettings),
middleware.ContentSecurityPolicy,
middleware.Cors(serverSettings),
middleware.Authenticate(authService),
middleware.CrossSiteRequestForgery(authService),
middleware.Corp,
middleware.Coop,
)
}

File diff suppressed because it is too large Load Diff

24
package-lock.json generated
View File

@@ -9,9 +9,9 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"daisyui": "4.12.22",
"htmx.org": "2.0.4",
"tailwindcss": "3.4.17"
"daisyui": "4.12.14",
"htmx.org": "2.0.3",
"tailwindcss": "3.4.16"
}
},
"node_modules/@alloc/quick-lru": {
@@ -344,9 +344,9 @@
}
},
"node_modules/daisyui": {
"version": "4.12.22",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.22.tgz",
"integrity": "sha512-HDLWbmTnXxhE1MrMgSWjVgdRt+bVYHvfNbW3GTsyIokRSqTHonUTrxV3RhpPDjGIWaHt+ELtDCTYCtUFgL2/Nw==",
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.14.tgz",
"integrity": "sha512-hA27cdBasdwd4/iEjn+aidoCrRroDuo3G5W9NDKaVCJI437Mm/3eSL/2u7MkZ0pt8a+TrYF3aT2pFVemTS3how==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -527,9 +527,9 @@
}
},
"node_modules/htmx.org": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
"integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz",
"integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA==",
"dev": true,
"license": "0BSD"
},
@@ -1240,9 +1240,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"version": "3.4.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
"integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -12,8 +12,8 @@
"author": "",
"license": "ISC",
"devDependencies": {
"htmx.org": "2.0.4",
"tailwindcss": "3.4.17",
"daisyui": "4.12.22"
"htmx.org": "2.0.3",
"tailwindcss": "3.4.16",
"daisyui": "4.12.14"
}
}

View File

@@ -18,33 +18,62 @@ import (
)
var (
ErrInvalidCredentials = errors.New("invalid email or password")
ErrInvalidPassword = errors.New("password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character")
ErrInvalidEmail = errors.New("invalid email")
ErrAccountExists = errors.New("account already exists")
ErrSessionIdInvalid = errors.New("session ID is invalid")
ErrTokenInvalid = errors.New("token is invalid")
ErrInvaidCredentials = errors.New("invalid email or password")
ErrInvalidPassword = errors.New("password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character")
ErrInvalidEmail = errors.New("invalid email")
ErrAccountExists = errors.New("account already exists")
ErrSessionIdInvalid = errors.New("session ID is invalid")
)
type User struct {
Id uuid.UUID
Email string
EmailVerified bool
}
func NewUser(user *db.User) *User {
return &User{
Id: user.Id,
Email: user.Email,
EmailVerified: user.EmailVerified,
}
}
type Session struct {
Id string
CreatedAt time.Time
ExpiresAt time.Time
User *User
}
func NewSession(session *db.Session, user *User) *Session {
return &Session{
Id: session.Id,
CreatedAt: session.CreatedAt,
ExpiresAt: session.ExpiresAt,
User: user,
}
}
type Auth interface {
SignUp(email string, password string) (*types.User, error)
SignUp(email string, password string) (*User, error)
SendVerificationMail(userId uuid.UUID, email string)
VerifyUserEmail(token string) error
SignIn(session *types.Session, email string, password string) (*types.Session, *types.User, error)
SignInSession(sessionId string) (*types.Session, *types.User, error)
SignInAnonymous() (*types.Session, error)
SignIn(email string, password string) (*Session, error)
SignInSession(sessionId string) (*Session, error)
SignInAnonymous() (*Session, error)
SignOut(sessionId string) error
DeleteAccount(user *types.User, currPass string) error
DeleteAccount(user *User) error
ChangePassword(user *types.User, sessionId string, currPass, newPass string) error
ChangePassword(user *User, currPass, newPass string) error
SendForgotPasswordMail(email string) error
ForgotPassword(token string, newPass string) error
IsCsrfTokenValid(tokenStr string, sessionId string) bool
GetCsrfToken(session *types.Session) (string, error)
GetCsrfToken(session *Session) (string, error)
}
type AuthImpl struct {
@@ -65,103 +94,76 @@ func NewAuthImpl(db db.Auth, random Random, clock Clock, mail Mail, serverSettin
}
}
func (service AuthImpl) SignIn(session *types.Session, email string, password string) (*types.Session, *types.User, error) {
func (service AuthImpl) SignIn(email string, password string) (*Session, error) {
user, err := service.db.GetUserByEmail(email)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return nil, nil, ErrInvalidCredentials
return nil, ErrInvaidCredentials
} else {
return nil, nil, types.ErrInternal
return nil, types.ErrInternal
}
}
hash := GetHashPassword(password, user.Salt)
if subtle.ConstantTimeCompare(hash, user.Password) == 0 {
return nil, nil, ErrInvalidCredentials
return nil, ErrInvaidCredentials
}
err = service.cleanUpSessionWithTokens(session)
if err != nil {
return nil, nil, types.ErrInternal
}
session, err = service.createSession(user.Id)
if err != nil {
return nil, nil, types.ErrInternal
}
return session, user, nil
}
func (service AuthImpl) cleanUpSessionWithTokens(session *types.Session) error {
if session == nil {
return nil
}
err := service.db.DeleteSession(session.Id)
if err != nil {
return types.ErrInternal
}
tokens, err := service.db.GetTokensBySessionIdAndType(session.Id, types.TokenTypeCsrf)
if err != nil {
return types.ErrInternal
}
for _, token := range tokens {
err = service.db.DeleteToken(token.Token)
if err != nil {
return types.ErrInternal
}
}
return nil
}
func (service AuthImpl) SignInSession(sessionId string) (*types.Session, *types.User, error) {
if sessionId == "" {
return nil, nil, ErrSessionIdInvalid
}
session, err := service.db.GetSession(sessionId)
if err != nil {
return nil, nil, types.ErrInternal
}
if session.ExpiresAt.Before(service.clock.Now()) {
_ = service.db.DeleteSession(sessionId)
return nil, nil, nil
}
if session.UserId == uuid.Nil {
return session, nil, nil
}
user, err := service.db.GetUser(session.UserId)
if err != nil {
return nil, nil, types.ErrInternal
}
return session, user, nil
}
func (service AuthImpl) SignInAnonymous() (*types.Session, error) {
session, err := service.createSession(uuid.Nil)
session, err := service.createSession(user.Id)
if err != nil {
return nil, types.ErrInternal
}
log.Info("Anonymous session created: %v", session.Id)
return NewSession(session, NewUser(user)), nil
}
func (service AuthImpl) SignInSession(sessionId string) (*Session, error) {
if sessionId == "" {
return nil, ErrSessionIdInvalid
}
sessionDb, err := service.db.GetSession(sessionId)
if err != nil {
return nil, types.ErrInternal
}
if sessionDb.ExpiresAt.Before(service.clock.Now()) {
return nil, nil
}
if sessionDb.UserId == uuid.Nil {
return NewSession(sessionDb, nil), nil
}
userDb, err := service.db.GetUser(sessionDb.UserId)
if err != nil {
return nil, types.ErrInternal
}
user := NewUser(userDb)
session := NewSession(sessionDb, user)
return session, nil
}
func (service AuthImpl) createSession(userId uuid.UUID) (*types.Session, error) {
func (service AuthImpl) SignInAnonymous() (*Session, error) {
sessionDb, err := service.createSession(uuid.Nil)
if err != nil {
return nil, types.ErrInternal
}
return NewSession(sessionDb, nil), nil
}
func (service AuthImpl) createSession(userId uuid.UUID) (*db.Session, error) {
sessionId, err := service.random.String(32)
if err != nil {
return nil, types.ErrInternal
}
err = service.db.DeleteOldSessions(userId)
if err != nil {
return nil, types.ErrInternal
}
@@ -169,7 +171,7 @@ func (service AuthImpl) createSession(userId uuid.UUID) (*types.Session, error)
createAt := service.clock.Now()
expiresAt := createAt.Add(24 * time.Hour)
session := types.NewSession(sessionId, userId, createAt, expiresAt)
session := db.NewSession(sessionId, userId, createAt, expiresAt)
err = service.db.InsertSession(session)
if err != nil {
@@ -179,7 +181,7 @@ func (service AuthImpl) createSession(userId uuid.UUID) (*types.Session, error)
return session, nil
}
func (service AuthImpl) SignUp(email string, password string) (*types.User, error) {
func (service AuthImpl) SignUp(email string, password string) (*User, error) {
_, err := mail.ParseAddress(email)
if err != nil {
return nil, ErrInvalidEmail
@@ -201,9 +203,9 @@ func (service AuthImpl) SignUp(email string, password string) (*types.User, erro
hash := GetHashPassword(password, salt)
user := types.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now())
dbUser := db.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now())
err = service.db.InsertUser(user)
err = service.db.InsertUser(dbUser)
if err != nil {
if err == db.ErrAlreadyExists {
return nil, ErrAccountExists
@@ -212,17 +214,17 @@ func (service AuthImpl) SignUp(email string, password string) (*types.User, erro
}
}
return user, nil
return NewUser(dbUser), nil
}
func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
tokens, err := service.db.GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify)
if err != nil && err != db.ErrNotFound {
tokens, err := service.db.GetTokensByUserIdAndType(userId, db.TokenTypeEmailVerify)
if err != nil {
return
}
var token *types.Token
var token *db.Token
if len(tokens) > 0 {
token = tokens[0]
@@ -234,7 +236,7 @@ func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
return
}
token = types.NewToken(userId, "", newTokenStr, types.TokenTypeEmailVerify, service.clock.Now(), service.clock.Now().Add(24*time.Hour))
token = db.NewToken(userId, "", newTokenStr, db.TokenTypeEmailVerify, service.clock.Now(), service.clock.Now().Add(24*time.Hour))
err = service.db.InsertToken(token)
if err != nil {
@@ -268,7 +270,7 @@ func (service AuthImpl) VerifyUserEmail(tokenStr string) error {
return types.ErrInternal
}
if token.Type != types.TokenTypeEmailVerify {
if token.Type != db.TokenTypeEmailVerify {
return types.ErrInternal
}
@@ -295,29 +297,19 @@ func (service AuthImpl) SignOut(sessionId string) error {
return service.db.DeleteSession(sessionId)
}
func (service AuthImpl) DeleteAccount(user *types.User, currPass string) error {
func (service AuthImpl) DeleteAccount(user *User) error {
userDb, err := service.db.GetUser(user.Id)
if err != nil {
return types.ErrInternal
}
currHash := GetHashPassword(currPass, userDb.Salt)
if subtle.ConstantTimeCompare(currHash, userDb.Password) == 0 {
return ErrInvalidCredentials
}
err = service.db.DeleteUser(user.Id)
err := service.db.DeleteUser(user.Id)
if err != nil {
return err
}
service.mail.SendMail(user.Email, "Account deleted", "Your account has been deleted")
go service.mail.SendMail(user.Email, "Account deleted", "Your account has been deleted")
return nil
}
func (service AuthImpl) ChangePassword(user *types.User, sessionId string, currPass, newPass string) error {
func (service AuthImpl) ChangePassword(user *User, currPass, newPass string) error {
if !isPasswordValid(newPass) {
return ErrInvalidPassword
@@ -327,37 +319,30 @@ func (service AuthImpl) ChangePassword(user *types.User, sessionId string, currP
return ErrInvalidPassword
}
currHash := GetHashPassword(currPass, user.Salt)
if subtle.ConstantTimeCompare(currHash, user.Password) == 0 {
return ErrInvalidCredentials
}
newHash := GetHashPassword(newPass, user.Salt)
user.Password = newHash
err := service.db.UpdateUser(user)
_, err := service.SignIn(user.Email, currPass)
if err != nil {
return err
}
sessions, err := service.db.GetSessions(user.Id)
userDb, err := service.db.GetUser(user.Id)
if err != nil {
return types.ErrInternal
return err
}
for _, s := range sessions {
if s.Id != sessionId {
err = service.db.DeleteSession(s.Id)
if err != nil {
return types.ErrInternal
}
}
newHash := GetHashPassword(newPass, userDb.Salt)
userDb.Password = newHash
err = service.db.UpdateUser(userDb)
if err != nil {
return err
}
return nil
}
func (service AuthImpl) SendForgotPasswordMail(email string) error {
tokenStr, err := service.random.String(32)
if err != nil {
return err
@@ -372,7 +357,7 @@ func (service AuthImpl) SendForgotPasswordMail(email string) error {
}
}
token := types.NewToken(user.Id, "", tokenStr, types.TokenTypePasswordReset, service.clock.Now(), service.clock.Now().Add(15*time.Minute))
token := db.NewToken(user.Id, "", tokenStr, db.TokenTypePasswordReset, service.clock.Now(), service.clock.Now().Add(15*time.Minute))
err = service.db.InsertToken(token)
if err != nil {
@@ -385,7 +370,7 @@ func (service AuthImpl) SendForgotPasswordMail(email string) error {
log.Error("Could not render reset password email: %v", err)
return types.ErrInternal
}
service.mail.SendMail(email, "Reset Password", mail.String())
go service.mail.SendMail(email, "Reset Password", mail.String())
return nil
}
@@ -398,7 +383,7 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
token, err := service.db.GetToken(tokenStr)
if err != nil {
return ErrTokenInvalid
return err
}
err = service.db.DeleteToken(tokenStr)
@@ -406,11 +391,6 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
return err
}
if token.Type != types.TokenTypePasswordReset ||
token.ExpiresAt.Before(service.clock.Now()) {
return ErrTokenInvalid
}
user, err := service.db.GetUser(token.UserId)
if err != nil {
log.Error("Could not get user from token: %v", err)
@@ -425,18 +405,6 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
return err
}
sessions, err := service.db.GetSessions(user.Id)
if err != nil {
return types.ErrInternal
}
for _, session := range sessions {
err = service.db.DeleteSession(session.Id)
if err != nil {
return types.ErrInternal
}
}
return nil
}
@@ -446,7 +414,7 @@ func (service AuthImpl) IsCsrfTokenValid(tokenStr string, sessionId string) bool
return false
}
if token.Type != types.TokenTypeCsrf ||
if token.Type != db.TokenTypeCsrf ||
token.SessionId != sessionId ||
token.ExpiresAt.Before(service.clock.Now()) {
@@ -456,12 +424,12 @@ func (service AuthImpl) IsCsrfTokenValid(tokenStr string, sessionId string) bool
return true
}
func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) {
func (service AuthImpl) GetCsrfToken(session *Session) (string, error) {
if session == nil {
return "", types.ErrInternal
}
tokens, _ := service.db.GetTokensBySessionIdAndType(session.Id, types.TokenTypeCsrf)
tokens, _ := service.db.GetTokensBySessionIdAndType(session.Id, db.TokenTypeCsrf)
if len(tokens) > 0 {
return tokens[0].Token, nil
@@ -472,14 +440,12 @@ func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) {
return "", types.ErrInternal
}
token := types.NewToken(session.UserId, session.Id, tokenStr, types.TokenTypeCsrf, service.clock.Now(), service.clock.Now().Add(8*time.Hour))
token := db.NewToken(uuid.Nil, session.Id, tokenStr, db.TokenTypeCsrf, service.clock.Now(), service.clock.Now().Add(24*time.Hour))
err = service.db.InsertToken(token)
if err != nil {
return "", types.ErrInternal
}
log.Info("CSRF-Token created: %v", tokenStr)
return tokenStr, nil
}

View File

@@ -5,6 +5,7 @@ import (
"me-fit/mocks"
"me-fit/types"
"errors"
"strings"
"testing"
"time"
@@ -14,6 +15,104 @@ import (
"github.com/stretchr/testify/mock"
)
func TestSignIn(t *testing.T) {
t.Parallel()
t.Run("should return user if password is correct", func(t *testing.T) {
t.Parallel()
salt := []byte("salt")
verifiedAt := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
user := db.NewUser(
uuid.New(),
"test@test.de",
true,
&verifiedAt,
false,
GetHashPassword("password", salt),
salt,
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
)
dbSession := db.NewSession("sessionId", user.Id, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC))
mockAuthDb := mocks.NewMockAuth(t)
mockAuthDb.EXPECT().GetUserByEmail("test@test.de").Return(user, nil)
mockAuthDb.EXPECT().DeleteOldSessions(user.Id).Return(nil)
mockAuthDb.EXPECT().InsertSession(dbSession).Return(nil)
mockRandom := mocks.NewMockRandom(t)
mockRandom.EXPECT().String(32).Return("sessionId", nil)
mockClock := mocks.NewMockClock(t)
mockClock.EXPECT().Now().Return(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))
mockMail := mocks.NewMockMail(t)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
actualSession, err := underTest.SignIn(user.Email, "password")
assert.Nil(t, err)
expectedSession := NewSession(dbSession, NewUser(user))
assert.Equal(t, expectedSession, actualSession)
})
t.Run("should return ErrInvalidCretentials if password is not correct", func(t *testing.T) {
t.Parallel()
salt := []byte("salt")
verifiedAt := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
user := db.NewUser(
uuid.New(),
"test@test.de",
true,
&verifiedAt,
false,
GetHashPassword("password", salt),
salt,
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
)
mockAuthDb := mocks.NewMockAuth(t)
mockAuthDb.EXPECT().GetUserByEmail(user.Email).Return(user, nil)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
_, err := underTest.SignIn("test@test.de", "wrong password")
assert.Equal(t, ErrInvaidCredentials, err)
})
t.Run("should return ErrInvalidCretentials if user has not been found", func(t *testing.T) {
t.Parallel()
mockAuthDb := mocks.NewMockAuth(t)
mockAuthDb.EXPECT().GetUserByEmail("test").Return(nil, db.ErrNotFound)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
_, err := underTest.SignIn("test", "test")
assert.Equal(t, ErrInvaidCredentials, err)
})
t.Run("should forward ErrInternal on any other error", func(t *testing.T) {
t.Parallel()
mockAuthDb := mocks.NewMockAuth(t)
mockAuthDb.EXPECT().GetUserByEmail("test").Return(nil, errors.New("Some undefined error"))
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
_, err := underTest.SignIn("test", "test")
assert.Equal(t, types.ErrInternal, err)
})
}
func TestSignUp(t *testing.T) {
t.Parallel()
t.Run("should check for correct email address", func(t *testing.T) {
@@ -60,25 +159,33 @@ func TestSignUp(t *testing.T) {
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
userId := uuid.New()
email := "mail@mail.de"
expected := User{
Id: uuid.New(),
Email: "some@valid.email",
EmailVerified: false,
}
random := NewRandomImpl()
salt, err := random.Bytes(16)
assert.Nil(t, err)
password := "SomeStrongPassword123!"
salt := []byte("salt")
mockRandom.EXPECT().UUID().Return(expected.Id, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
expected := types.NewUser(userId, email, false, nil, false, GetHashPassword(password, salt), salt, createTime)
mockRandom.EXPECT().UUID().Return(userId, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(expected).Return(nil)
mockAuthDb.EXPECT().InsertUser(db.NewUser(expected.Id, expected.Email, false, nil, false, GetHashPassword(password, salt), salt, createTime)).Return(nil)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
actual, err := underTest.SignUp(email, password)
actual, err := underTest.SignUp(expected.Email, password)
assert.Nil(t, err)
assert.Equal(t, expected, actual)
assert.Equal(t, expected, *actual)
})
t.Run("should return ErrAccountExists", func(t *testing.T) {
t.Parallel()
@@ -88,22 +195,28 @@ func TestSignUp(t *testing.T) {
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
userId := uuid.New()
email := "some@valid.email"
createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
user := User{
Id: uuid.New(),
Email: "some@valid.email",
}
random := NewRandomImpl()
salt, err := random.Bytes(16)
assert.Nil(t, err)
password := "SomeStrongPassword123!"
salt := []byte("salt")
user := types.NewUser(userId, email, false, nil, false, GetHashPassword(password, salt), salt, createTime)
mockRandom.EXPECT().UUID().Return(user.Id, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(user).Return(db.ErrAlreadyExists)
mockAuthDb.EXPECT().InsertUser(db.NewUser(user.Id, user.Email, false, nil, false, GetHashPassword(password, salt), salt, createTime)).Return(db.ErrAlreadyExists)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
_, err := underTest.SignUp(user.Email, password)
_, err = underTest.SignUp(user.Email, password)
assert.Equal(t, ErrAccountExists, err)
})
}
@@ -114,8 +227,8 @@ func TestSendVerificationMail(t *testing.T) {
t.Run("should use stored token and send mail", func(t *testing.T) {
t.Parallel()
token := types.NewToken(uuid.New(), "sessionId", "someRandomTokenToUse", types.TokenTypeEmailVerify, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC))
tokens := []*types.Token{token}
token := db.NewToken(uuid.New(), "sessionId", "someRandomTokenToUse", db.TokenTypeEmailVerify, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC))
tokens := []*db.Token{token}
email := "some@email.de"
userId := uuid.New()
@@ -125,7 +238,7 @@ func TestSendVerificationMail(t *testing.T) {
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify).Return(tokens, nil)
mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, db.TokenTypeEmailVerify).Return(tokens, nil)
mockMail.EXPECT().SendMail(email, "Welcome to ME-FIT", mock.MatchedBy(func(message string) bool {
return strings.Contains(message, token.Token)

View File

@@ -22,10 +22,6 @@ func NewMailImpl(server *types.Settings) MailImpl {
}
func (m MailImpl) SendMail(to string, subject string, message string) {
go m.internalSendMail(to, subject, message)
}
func (m MailImpl) internalSendMail(to string, subject string, message string) {
if m.server.Smtp == nil {
return
}
@@ -36,7 +32,6 @@ func (m MailImpl) internalSendMail(to string, subject string, message string) {
msg := fmt.Sprintf("From: %v <%v>\nTo: %v\nSubject: %v\nMIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n%v", s.FromName, s.FromMail, to, subject, message)
log.Info("Sending mail to %v", to)
err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg))
if err != nil {
log.Error("Error sending mail: %v", err)

View File

@@ -37,7 +37,6 @@ func (r *RandomImpl) Bytes(size int) ([]byte, error) {
func (r *RandomImpl) String(size int) (string, error) {
bytes, err := r.Bytes(size)
if err != nil {
log.Error("Error generating random string: %v", err)
return "", types.ErrInternal
}

View File

@@ -10,9 +10,9 @@ import (
)
type Workout interface {
AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error)
DeleteWorkout(user *types.User, rowId int) error
GetWorkouts(user *types.User) ([]*WorkoutDto, error)
AddWorkout(user *User, workoutDto *WorkoutDto) (*WorkoutDto, error)
DeleteWorkout(user *User, rowId int) error
GetWorkouts(user *User) ([]*WorkoutDto, error)
}
type WorkoutImpl struct {
@@ -64,7 +64,7 @@ var (
ErrInputValues = errors.New("invalid input values")
)
func (service WorkoutImpl) AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error) {
func (service WorkoutImpl) AddWorkout(user *User, workoutDto *WorkoutDto) (*WorkoutDto, error) {
if workoutDto.Date == "" || workoutDto.Type == "" || workoutDto.Sets == "" || workoutDto.Reps == "" {
return nil, ErrInputValues
@@ -95,7 +95,7 @@ func (service WorkoutImpl) AddWorkout(user *types.User, workoutDto *WorkoutDto)
return NewWorkoutDtoFromDb(workout), nil
}
func (service WorkoutImpl) DeleteWorkout(user *types.User, rowId int) error {
func (service WorkoutImpl) DeleteWorkout(user *User, rowId int) error {
if user == nil {
return types.ErrInternal
}
@@ -103,7 +103,7 @@ func (service WorkoutImpl) DeleteWorkout(user *types.User, rowId int) error {
return service.db.DeleteWorkout(user.Id, rowId)
}
func (service WorkoutImpl) GetWorkouts(user *types.User) ([]*WorkoutDto, error) {
func (service WorkoutImpl) GetWorkouts(user *User) ([]*WorkoutDto, error) {
if user == nil {
return nil, types.ErrInternal
}

View File

@@ -4,7 +4,7 @@ templ ChangePasswordComp(isPasswordReset bool) {
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
if isPasswordReset {
hx-post="/api/auth/forgot-password-actual"
hx-post="/api/auth/reset-password-actual"
} else {
hx-post="/api/auth/change-password"
}
@@ -15,29 +15,11 @@ templ ChangePasswordComp(isPasswordReset bool) {
</h2>
if !isPasswordReset {
<label class="input input-bordered flex items-center gap-2">
<input
type="password"
class="grow"
placeholder="Current Password"
name="current-password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
<input type="password" class="grow" placeholder="Current Password" name="current-password"/>
</label>
}
<label class="input input-bordered flex items-center gap-2">
<input
type="password"
class="grow"
placeholder="New Password"
name="new-password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
<input type="password" class="grow" placeholder="New Password" name="new-password"/>
</label>
<button class="btn btn-primary self-end">
Change Password

View File

@@ -13,16 +13,7 @@ templ DeleteAccountComp() {
Are you sure you want to delete your account? This action is irreversible.
</p>
<label class="input input-bordered flex items-center gap-2">
<input
type="password"
class="grow"
placeholder="Password"
name="password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
<input type="password" class="grow" placeholder="Password" name="password"/>
</label>
<button class="btn btn-error self-end">
Delete Account

View File

@@ -3,23 +3,14 @@ package auth
templ ResetPasswordComp() {
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
hx-post="/api/auth/forgot-password"
hx-post="/api/auth/reset-password"
hx-swap="none"
>
<h2 class="text-6xl mb-10">
Reset Password
</h2>
<label class="input input-bordered flex items-center gap-2">
<input
type="email"
class="grow"
placeholder="E-Mail"
name="email"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
<input type="email" class="grow" placeholder="E-Mail" name="email"/>
</label>
<button class="btn btn-primary self-end">
Request Password Reset

View File

@@ -1,18 +1,14 @@
package auth
templ SignInOrUpComp(isSignIn bool) {
{{
var postUrl string
if isSignIn {
postUrl = "/api/auth/signin"
} else {
postUrl = "/api/auth/signup"
}
}}
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
hx-target="#sign-in-or-up-error"
hx-post={ postUrl }
if isSignIn {
hx-post="/api/auth/signin"
} else {
hx-post="/api/auth/signup"
}
>
<h2 class="text-6xl mb-10">
if isSignIn {
@@ -22,7 +18,12 @@ if isSignIn {
}
</h2>
<label class="input input-bordered flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-4 w-4 opacity-70">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"
></path>
@@ -30,39 +31,26 @@ if isSignIn {
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"
></path>
</svg>
<input
type="text"
class="grow"
placeholder="Email"
name="email"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
<input type="text" class="grow" placeholder="Email" name="email"/>
</label>
<label class="input input-bordered flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-4 w-4 opacity-70">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd"
></path>
</svg>
<input
type="password"
class="grow"
placeholder="Password"
name="password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
<input type="password" class="grow" placeholder="Password" name="password"/>
</label>
<div class="flex justify-end items-center gap-2">
if isSignIn {
<a href="/auth/forgot-password" class="grow link text-gray-500 text-sm">Forgot Password?</a>
<a href="/auth/reset-password" class="grow link text-gray-500 text-sm">Forgot Password?</a>
<a href="/auth/signup" class="link text-gray-500 text-sm">Don't have an account? Sign Up</a>
<button class="btn btn-primary">
Sign In

View File

@@ -1,30 +1,38 @@
package auth
templ UserComp(user string) {
<div id="user-info" class="flex gap-5 items-center">
if user != "" {
<div class="group inline-block relative">
<button class="font-semibold py-2 px-4 inline-flex items-center">
<span class="mr-1">{ user }</span>
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path>
</svg>
</button>
<div class="absolute hidden group-hover:block w-full">
<ul class="menu bg-base-300 rounded-box w-fit float-right mr-4 p-3">
<li class="mb-1">
<a hx-post="/api/auth/signout" hx-target="#user-info">Sign Out</a>
</li>
<li class="mb-1">
<a href="/auth/change-password">Change Password</a>
</li>
<li><a href="/auth/delete-account" class="text-error">Delete Account</a></li>
</ul>
</div>
<div id="user-info" class="flex gap-5 items-center">
if user != "" {
<div class="group inline-block relative">
<button
class="font-semibold py-2 px-4 inline-flex items-center"
>
<span class="mr-1">{ user }</span>
<svg
class="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path
d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
></path>
</svg>
</button>
<div class="absolute hidden group-hover:block w-full">
<ul class="menu bg-base-300 rounded-box w-fit float-right mr-4 p-3">
<li class="mb-1">
<a hx-get="/api/auth/signout" hx-target="#user-info">Sign Out</a>
</li>
<li class="mb-1">
<a href="/auth/change-password">Change Password</a>
</li>
<li><a href="/auth/delete-account" class="text-error">Delete Account</a></li>
</ul>
</div>
</div>
} else {
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
}
</div>
} else {
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
}
</div>
}

View File

@@ -1,29 +0,0 @@
package auth
templ VerifyResponseComp(isVerified bool) {
<main>
<div class="flex flex-col items-center justify-center h-screen">
if isVerified {
<h2 class="text-6xl mb-10">
Your email has been verified
</h2>
<p class="text-lg text-center">
You have completed the verification process. Thank you!
</p>
<a class="btn btn-primary mt-8" href="/">
Go Home
</a>
} else {
<h2 class="text-6xl mb-10">
Error during verification
</h2>
<p class="text-lg text-center">
Please try again by sign up process
</p>
<a class="btn btn-primary mt-8" href="/auth/signup">
Sign Up
</a>
}
</div>
</main>
}

View File

@@ -1,48 +1,48 @@
package template
templ Layout(slot templ.Component, user templ.Component, environment string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>ME-FIT</title>
<link rel="icon" href="/static/favicon.svg" />
<link rel="stylesheet" href="/static/css/tailwind.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
if environment == "prod" {
<script defer src="https://umami.me-fit.eu/script.js" data-website-id="3c8efb09-44e4-4372-8a1e-c3bc675cd89a"></script>
}
<meta name="htmx-config" content='{
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>ME-FIT</title>
<link rel="icon" href="/static/favicon.svg"/>
<link rel="stylesheet" href="/static/css/tailwind.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
if environment == "prod" {
<script defer src="https://umami.me-fit.eu/script.js" data-website-id="3c8efb09-44e4-4372-8a1e-c3bc675cd89a"></script>
}
<meta
name="htmx-config"
content='{
"includeIndicatorStyles": false,
"selfRequestsOnly": true,
"allowScriptTags": false
}' />
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script>
</head>
<body hx-headers='{"csrf-token": "CSRF_TOKEN"}'>
<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">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="ME-FIT logo" />
<span>ME-FIT</span>
</a>
@user
</div>
<div class="flex-1">
if slot != nil {
@slot
}
</div>
</div>
<div class="toast" id="toasts">
<div class="hidden alert" id="toast">
New message arrived.
</div>
</div>
</body>
</html>
}'
/>
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script>
</head>
<body>
<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">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="ME-FIT logo"/>
<span>ME-FIT</span>
</a>
@user
</div>
<div class="flex-1">
if slot != nil {
@slot
}
</div>
</div>
<div class="toast" id="toasts">
<div class="hidden alert" id="toast">
New message arrived.
</div>
</div>
</body>
</html>
}

View File

@@ -3,21 +3,17 @@ package mail;
import "net/url"
templ Register(baseUrl string, token string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome</title>
</head>
<body>
<h4>Thank you for Sign Up!</h4>
<p>Click <a href={ templ.URL(baseUrl + "/auth/verify-email?token=" + url.QueryEscape(token)) }>here</a> to finalize
your registration.</p>
<p>Kind regards</p>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Welcome</title>
</head>
<body>
<h4>Thank you for Sign Up!</h4>
<p>Click <a href={ templ.URL(baseUrl + "/auth/verify-email?token=" + url.QueryEscape(token)) }>here</a> to verify your account.</p>
<p>Kind regards</p>
</body>
</html>
}

View File

@@ -1,69 +1,73 @@
package workout
templ WorkoutComp(currentDate string) {
<main class="mx-2">
<form class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10" hx-post="/api/workout"
hx-target="#workout-placeholder" hx-swap="outerHTML">
<h2 class="text-4xl mb-8">Track your workout</h2>
<input id="date" type="date" class="input input-bordered" value={ currentDate } name="date" />
<select class="select select-bordered w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input type="number" class="input input-bordered" placeholder="Sets" name="sets" />
<input type="number" class="input input-bordered" placeholder="Reps" name="reps" />
<button class="btn btn-primary self-end">Save</button>
</form>
<div hx-get="/api/workout" hx-trigger="load"></div>
</main>
<main class="mx-2">
<form
class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10"
hx-post="/api/workout"
hx-target="#workout-placeholder"
hx-swap="outerHTML"
>
<h2 class="text-4xl mb-8">Track your workout</h2>
<input id="date" type="date" class="input input-bordered" value={ currentDate } name="date"/>
<select class="select select-bordered w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input type="number" class="input input-bordered" placeholder="Sets" name="sets"/>
<input type="number" class="input input-bordered" placeholder="Reps" name="reps"/>
<button class="btn btn-primary self-end">Save</button>
</form>
<div hx-get="/api/workout" hx-trigger="load"></div>
</main>
}
type Workout struct {
Id string
Date string
Type string
Sets string
Reps string
Id string
Date string
Type string
Sets string
Reps string
}
templ WorkoutListComp(workouts []Workout) {
<div class="overflow-x-auto mx-auto max-w-screen-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sets</th>
<th>Reps</th>
<th></th>
</tr>
</thead>
<tbody>
<tr class="hidden" id="workout-placeholder"></tr>
for _,w := range workouts {
@WorkoutItemComp(w, false)
}
</tbody>
</table>
</div>
<div class="overflow-x-auto mx-auto max-w-screen-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sets</th>
<th>Reps</th>
<th></th>
</tr>
</thead>
<tbody>
<tr class="hidden" id="workout-placeholder"></tr>
for _,w := range workouts {
@WorkoutItemComp(w, false)
}
</tbody>
</table>
</div>
}
templ WorkoutItemComp(w Workout, includePlaceholder bool) {
if includePlaceholder {
<tr class="hidden" id="workout-placeholder"></tr>
}
<tr>
<th>{ w.Date }</th>
<th>{ w.Type }</th>
<th>{ w.Sets }</th>
<th>{ w.Reps }</th>
<th>
<div class="tooltip" data-tip="Delete Entry">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr" type="submit">
Delete
</button>
</div>
</th>
</tr>
if includePlaceholder {
<tr class="hidden" id="workout-placeholder"></tr>
}
<tr>
<th>{ w.Date }</th>
<th>{ w.Type }</th>
<th>{ w.Sets }</th>
<th>{ w.Reps }</th>
<th>
<div class="tooltip" data-tip="Delete Entry">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr">
Delete
</button>
</div>
</th>
</tr>
}

View File

@@ -1,75 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
)
type User struct {
Id uuid.UUID
Email string
EmailVerified bool
EmailVerifiedAt *time.Time
IsAdmin bool
Password []byte
Salt []byte
CreateAt time.Time
}
func NewUser(id uuid.UUID, email string, emailVerified bool, emailVerifiedAt *time.Time, isAdmin bool, password []byte, salt []byte, createAt time.Time) *User {
return &User{
Id: id,
Email: email,
EmailVerified: emailVerified,
EmailVerifiedAt: emailVerifiedAt,
IsAdmin: isAdmin,
Password: password,
Salt: salt,
CreateAt: createAt,
}
}
type Session struct {
Id string
UserId uuid.UUID
CreatedAt time.Time
ExpiresAt time.Time
}
func NewSession(id string, userId uuid.UUID, createdAt time.Time, expiresAt time.Time) *Session {
return &Session{
Id: id,
UserId: userId,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
}
}
type Token struct {
UserId uuid.UUID
SessionId string
Token string
Type TokenType
CreatedAt time.Time
ExpiresAt time.Time
}
type TokenType string
var (
TokenTypeEmailVerify TokenType = "email_verify"
TokenTypePasswordReset TokenType = "password_reset"
TokenTypeCsrf TokenType = "csrf"
)
func NewToken(userId uuid.UUID, sessionId string, token string, tokenType TokenType, createdAt time.Time, expiresAt time.Time) *Token {
return &Token{
UserId: userId,
SessionId: sessionId,
Token: token,
Type: tokenType,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
}
}

View File

@@ -1,17 +1,16 @@
package utils
import (
"me-fit/log"
"fmt"
"net/http"
"time"
"me-fit/log"
)
func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string, statusCode int) {
func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string) {
if isHtmx(r) {
w.Header().Set("HX-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, message))
w.WriteHeader(statusCode)
} else {
log.Error("Trying to trigger toast in non-HTMX request")
}