36 Commits

Author SHA1 Message Date
55408da398 chore(auth): #331 add and fix forgot password actual tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-25 23:13:58 +01:00
b0f183aeed chore(auth): #331 add and fix forgot password tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m9s
2024-12-25 22:58:37 +01:00
42a910df4b chore(deps): update node.js to 7bea049
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-25 22:26:02 +01:00
73333256c5 chore(deps): update golang:1.23.4 docker digest to b01f7c7
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-25 21:21:30 +00:00
14b477f560 chore(auth): #331 add change password tests
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-25 22:20:52 +01:00
87188724ac chore(deps): update debian:12.8 docker digest to b877a1a
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-12-25 20:58:34 +00:00
5ea400352f chore(auth): #331 add sign up verify tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-12-25 21:56:32 +01:00
397442767a chore(auth): #331 implement and fix sign up tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-24 22:39:26 +01:00
9462f8b245 chore(auth): #331 implement and fix fist sign up tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-23 22:57:41 +01:00
7a7d7cf204 chore(auth): #331 remove duplicated/outdated tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-23 22:36:55 +01:00
96b4cc6889 chore(auth): #331 add tests for sign in
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-12-23 22:33:10 +01:00
7a9d34d464 chore(auth): #331 add tests for sign in
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-12-23 13:56:41 +01:00
52cd85d904 chore(auth): #331 add tests for sign out
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
Build Docker Image / Build-Docker-Image (push) Successful in 46s
2024-12-22 23:48:53 +01:00
fb6cc0acda chore(auth): #331 add tests for delete account
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-22 23:07:15 +01:00
6a551929c5 chore(auth): #331 add and fix session tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-22 22:33:17 +01:00
ea653f0087 chore(auth): #331 unify existing tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-22 21:31:19 +01:00
143662fff0 fix(deps): update module golang.org/x/net to v0.33.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-18 23:01:53 +00:00
fdb955f20c feat: #337 unify types for auth module
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-18 23:44:59 +01:00
dcc5207272 feat(security): #328 delete old sessions for change and forgot password
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-18 22:56:51 +01:00
43d0a3d022 chore(test): add test for cache control and security headers 2024-12-18 22:56:48 +01:00
c48194c36f chore(deps): update dependency tailwindcss to v3.4.17
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-17 23:02:08 +00:00
23aa3d4b0e chore(test): add test for cache control and security headers
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-16 23:04:22 +01:00
9bb603970d chore(test): fix integration test 'waitForReady'
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-16 22:38:59 +01:00
6d3902e572 chore(test): update integration test setup to automatically generate ports
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-16 17:30:33 +01:00
88892ab6ca chore(deps): update dependency htmx.org to v2.0.4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-13 19:01:26 +00:00
28a97414d4 chore(deps): update dependency daisyui to v4.12.22
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m8s
2024-12-13 02:02:37 +00:00
f0ec293be8 feat(security): #314 include hsts
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-12 21:50:32 +01:00
1ad694ce2b feat(security): #314 include all proposed security headers
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-12 21:37:23 +01:00
60fe2789cc feat(security): #312 disable autofill for PII information
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-12 21:03:14 +01:00
5d83c9dcc0 chore(deps): update dependency daisyui to v4.12.21
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-12 16:01:32 +00:00
a8937a0e64 chore(deps): update golang:1.23.4 docker digest to 7003184
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-12 01:01:46 +00:00
9629e71962 fix(deps): update module golang.org/x/net to v0.32.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-12 00:01:42 +00:00
380dd979f6 feat(security): #305 don't cache sensitive data
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-12 00:02:55 +01:00
e81fa4b2b6 fix(deps): update module golang.org/x/crypto to v0.31.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-11 23:53:04 +01:00
5579e5da0c chore(deps): update dependency daisyui to v4.12.20
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-12-11 22:48:00 +00:00
12d7c13b02 feat(security): #286 use csrf token for delete request
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-11 15:49:11 +01:00
38 changed files with 2368 additions and 820 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.4@sha256:574185e5c6b9d09873f455a7c205ea0514bfd99738c5dc7750196403a44ed4b7 AS builder_go FROM golang:1.23.4@sha256:b01f7c744a3f1fccaf44905169169fed0ab13e6d1d702a6542d07b34cf677969 AS builder_go
WORKDIR /me-fit 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 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 RUN go install github.com/a-h/templ/cmd/templ@latest
@@ -13,7 +13,7 @@ RUN golangci-lint run ./...
RUN go build -o /me-fit/me-fit . RUN go build -o /me-fit/me-fit .
FROM node:22.12.0@sha256:35a5dd72bcac4bce43266408b58a02be6ff0b6098ffa6f5435aeea980a8951d7 AS builder_node FROM node:22.12.0@sha256:7bea049c66b5846c4ce2786b1b4e32865ef11b10fa446c1bfd791daea412c299 AS builder_node
WORKDIR /me-fit WORKDIR /me-fit
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm clean-install RUN npm clean-install
@@ -21,7 +21,7 @@ COPY . ./
RUN npm run build RUN npm run build
FROM debian:12.8@sha256:17122fe3d66916e55c0cbd5bbf54bb3f87b3582f4d86a755a0fd3498d360f91b FROM debian:12.8@sha256:b877a1a3fdf02469440f1768cf69c9771338a875b7add5e80c45b756c92ac20a
WORKDIR /me-fit WORKDIR /me-fit
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
COPY migration ./migration COPY migration ./migration

View File

@@ -17,89 +17,22 @@ var (
ErrAlreadyExists = errors.New("row already exists") 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 { type Auth interface {
InsertUser(user *User) error InsertUser(user *types.User) error
UpdateUser(user *User) error UpdateUser(user *types.User) error
GetUserByEmail(email string) (*User, error) GetUserByEmail(email string) (*types.User, error)
GetUser(userId uuid.UUID) (*User, error) GetUser(userId uuid.UUID) (*types.User, error)
DeleteUser(userId uuid.UUID) error DeleteUser(userId uuid.UUID) error
InsertToken(token *Token) error InsertToken(token *types.Token) error
GetToken(token string) (*Token, error) GetToken(token string) (*types.Token, error)
GetTokensByUserIdAndType(userId uuid.UUID, tokenType TokenType) ([]*Token, error) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
GetTokensBySessionIdAndType(sessionId string, tokenType TokenType) ([]*Token, error) GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error)
DeleteToken(token string) error DeleteToken(token string) error
InsertSession(session *Session) error InsertSession(session *types.Session) error
GetSession(sessionId string) (*Session, error) GetSession(sessionId string) (*types.Session, error)
GetSessions(userId uuid.UUID) ([]*types.Session, error)
DeleteSession(sessionId string) error DeleteSession(sessionId string) error
DeleteOldSessions(userId uuid.UUID) error DeleteOldSessions(userId uuid.UUID) error
} }
@@ -112,7 +45,7 @@ func NewAuthSqlite(db *sql.DB) *AuthSqlite {
return &AuthSqlite{db: db} return &AuthSqlite{db: db}
} }
func (db AuthSqlite) InsertUser(user *User) error { func (db AuthSqlite) InsertUser(user *types.User) error {
_, err := db.db.Exec(` _, err := db.db.Exec(`
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -130,7 +63,7 @@ func (db AuthSqlite) InsertUser(user *User) error {
return nil return nil
} }
func (db AuthSqlite) UpdateUser(user *User) error { func (db AuthSqlite) UpdateUser(user *types.User) error {
_, err := db.db.Exec(` _, err := db.db.Exec(`
UPDATE user UPDATE user
SET email_verified = ?, email_verified_at = ?, password = ? SET email_verified = ?, email_verified_at = ?, password = ?
@@ -145,7 +78,7 @@ func (db AuthSqlite) UpdateUser(user *User) error {
return nil return nil
} }
func (db AuthSqlite) GetUserByEmail(email string) (*User, error) { func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
var ( var (
userId uuid.UUID userId uuid.UUID
emailVerified bool emailVerified bool
@@ -169,10 +102,10 @@ func (db AuthSqlite) GetUserByEmail(email string) (*User, error) {
} }
} }
return NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
} }
func (db AuthSqlite) GetUser(userId uuid.UUID) (*User, error) { func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
var ( var (
email string email string
emailVerified bool emailVerified bool
@@ -196,7 +129,7 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*User, error) {
} }
} }
return NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
} }
func (db AuthSqlite) DeleteUser(userId uuid.UUID) error { func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
@@ -244,7 +177,7 @@ func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
return nil return nil
} }
func (db AuthSqlite) InsertToken(token *Token) error { func (db AuthSqlite) InsertToken(token *types.Token) error {
_, err := db.db.Exec(` _, err := db.db.Exec(`
INSERT INTO token (user_id, session_id, type, token, created_at, expires_at) 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) VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt)
@@ -257,11 +190,11 @@ func (db AuthSqlite) InsertToken(token *Token) error {
return nil return nil
} }
func (db AuthSqlite) GetToken(token string) (*Token, error) { func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
var ( var (
userId uuid.UUID userId uuid.UUID
sessionId string sessionId string
tokenType TokenType tokenType types.TokenType
createdAtStr string createdAtStr string
expiresAtStr string expiresAtStr string
createdAt time.Time createdAt time.Time
@@ -295,10 +228,10 @@ func (db AuthSqlite) GetToken(token string) (*Token, error) {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
return NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
} }
func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType TokenType) ([]*Token, error) { func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
query, err := db.db.Query(` query, err := db.db.Query(`
SELECT token, created_at, expires_at SELECT token, created_at, expires_at
@@ -314,7 +247,7 @@ func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType TokenT
return getTokensFromQuery(query, userId, "", tokenType) return getTokensFromQuery(query, userId, "", tokenType)
} }
func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType TokenType) ([]*Token, error) { func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
query, err := db.db.Query(` query, err := db.db.Query(`
SELECT token, created_at, expires_at SELECT token, created_at, expires_at
@@ -330,8 +263,8 @@ func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType Tok
return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType) return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType)
} }
func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType TokenType) ([]*Token, error) { func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
var tokens []*Token var tokens []*types.Token
hasRows := false hasRows := false
for query.Next() { for query.Next() {
@@ -363,7 +296,7 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok
return nil, types.ErrInternal return nil, types.ErrInternal
} }
tokens = append(tokens, NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt)) tokens = append(tokens, types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt))
} }
if !hasRows { if !hasRows {
@@ -382,7 +315,7 @@ func (db AuthSqlite) DeleteToken(token string) error {
return nil return nil
} }
func (db AuthSqlite) InsertSession(session *Session) error { func (db AuthSqlite) InsertSession(session *types.Session) error {
_, err := db.db.Exec(` _, err := db.db.Exec(`
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
@@ -396,7 +329,7 @@ func (db AuthSqlite) InsertSession(session *Session) error {
return nil return nil
} }
func (db AuthSqlite) GetSession(sessionId string) (*Session, error) { func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) {
var ( var (
userId uuid.UUID userId uuid.UUID
@@ -410,15 +343,51 @@ func (db AuthSqlite) GetSession(sessionId string) (*Session, error) {
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt) WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
if err != nil { if err != nil {
log.Warn("Session not found: %v", err)
return nil, ErrNotFound return nil, ErrNotFound
} }
return NewSession(sessionId, userId, createdAt, expiresAt), nil 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
} }
func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error { func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
// Delete old inactive sessions _, err := db.db.Exec(`
_, err := db.db.Exec("DELETE FROM session WHERE created_at < datetime('now','-8 hours') AND user_id = ?", userId) DELETE FROM session
WHERE expires_at < datetime('now')
AND user_id = ?`, userId)
if err != nil { if err != nil {
log.Error("Could not delete old sessions: %v", err) log.Error("Could not delete old sessions: %v", err)
return types.ErrInternal return types.ErrInternal
@@ -428,7 +397,6 @@ func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
func (db AuthSqlite) DeleteSession(sessionId string) error { func (db AuthSqlite) DeleteSession(sessionId string) error {
if sessionId != "" { if sessionId != "" {
_, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId) _, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId)
if err != nil { if err != nil {
log.Error("Could not delete session: %v", err) 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) verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expected := NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt) expected := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(expected) err := underTest.InsertUser(expected)
assert.Nil(t, err) 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) verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt) user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(user)
assert.Nil(t, err) assert.Nil(t, err)
@@ -83,7 +83,7 @@ func TestUser(t *testing.T) {
underTest := AuthSqlite{db: db} underTest := AuthSqlite{db: db}
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt) user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(user)
assert.Equal(t, types.ErrInternal, err) 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) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expiresAt := createAt.Add(24 * time.Hour) expiresAt := createAt.Add(24 * time.Hour)
expected := NewToken(uuid.New(), "sessionId", "token", TokenTypeCsrf, createAt, expiresAt) expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected) err := underTest.InsertToken(expected)
assert.Nil(t, err) assert.Nil(t, err)
@@ -113,13 +113,13 @@ func TestToken(t *testing.T) {
expected.SessionId = "" expected.SessionId = ""
actuals, err := underTest.GetTokensByUserIdAndType(expected.UserId, expected.Type) actuals, err := underTest.GetTokensByUserIdAndType(expected.UserId, expected.Type)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, []*Token{expected}, actuals) assert.Equal(t, []*types.Token{expected}, actuals)
expected.SessionId = "sessionId" expected.SessionId = "sessionId"
expected.UserId = uuid.Nil expected.UserId = uuid.Nil
actuals, err = underTest.GetTokensBySessionIdAndType(expected.SessionId, expected.Type) actuals, err = underTest.GetTokensBySessionIdAndType(expected.SessionId, expected.Type)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, []*Token{expected}, actuals) assert.Equal(t, []*types.Token{expected}, actuals)
}) })
t.Run("should insert and return multiple tokens", func(t *testing.T) { t.Run("should insert and return multiple tokens", func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -130,8 +130,8 @@ func TestToken(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expiresAt := createAt.Add(24 * time.Hour) expiresAt := createAt.Add(24 * time.Hour)
userId := uuid.New() userId := uuid.New()
expected1 := NewToken(userId, "sessionId", "token1", TokenTypeCsrf, createAt, expiresAt) expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt)
expected2 := NewToken(userId, "sessionId", "token2", TokenTypeCsrf, createAt, expiresAt) expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected1) err := underTest.InsertToken(expected1)
assert.Nil(t, err) assert.Nil(t, err)
@@ -142,7 +142,7 @@ func TestToken(t *testing.T) {
expected2.UserId = uuid.Nil expected2.UserId = uuid.Nil
actuals, err := underTest.GetTokensBySessionIdAndType(expected1.SessionId, expected1.Type) actuals, err := underTest.GetTokensBySessionIdAndType(expected1.SessionId, expected1.Type)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, []*Token{expected1, expected2}, actuals) assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
expected1.SessionId = "" expected1.SessionId = ""
expected2.SessionId = "" expected2.SessionId = ""
@@ -150,7 +150,7 @@ func TestToken(t *testing.T) {
expected2.UserId = userId expected2.UserId = userId
actuals, err = underTest.GetTokensByUserIdAndType(userId, expected1.Type) actuals, err = underTest.GetTokensByUserIdAndType(userId, expected1.Type)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, []*Token{expected1, expected2}, actuals) assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
}) })
t.Run("should return ErrNotFound", func(t *testing.T) { t.Run("should return ErrNotFound", func(t *testing.T) {
@@ -162,10 +162,10 @@ func TestToken(t *testing.T) {
_, err := underTest.GetToken("nonExistent") _, err := underTest.GetToken("nonExistent")
assert.Equal(t, ErrNotFound, err) assert.Equal(t, ErrNotFound, err)
_, err = underTest.GetTokensByUserIdAndType(uuid.New(), TokenTypeEmailVerify) _, err = underTest.GetTokensByUserIdAndType(uuid.New(), types.TokenTypeEmailVerify)
assert.Equal(t, ErrNotFound, err) assert.Equal(t, ErrNotFound, err)
_, err = underTest.GetTokensBySessionIdAndType("sessionId", TokenTypeEmailVerify) _, err = underTest.GetTokensBySessionIdAndType("sessionId", types.TokenTypeEmailVerify)
assert.Equal(t, ErrNotFound, err) assert.Equal(t, ErrNotFound, err)
}) })
t.Run("should return ErrAlreadyExists", func(t *testing.T) { 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) verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt) user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(user)
assert.Nil(t, err) assert.Nil(t, err)
@@ -191,7 +191,7 @@ func TestToken(t *testing.T) {
underTest := AuthSqlite{db: db} underTest := AuthSqlite{db: db}
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt) user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(user)
assert.Equal(t, types.ErrInternal, err) 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/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_golang v1.20.5
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.30.0 golang.org/x/crypto v0.31.0
golang.org/x/net v0.29.0 golang.org/x/net v0.33.0
) )
require ( require (

22
go.sum
View File

@@ -8,6 +8,8 @@ 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/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 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -19,6 +21,14 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -33,20 +43,24 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 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 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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) { func (handler AuthImpl) Handle(router *http.ServeMux) {
router.Handle("/auth/signin", handler.handleSignInPage()) router.Handle("GET /auth/signin", handler.handleSignInPage())
router.Handle("/api/auth/signin", handler.handleSignIn()) router.Handle("POST /api/auth/signin", handler.handleSignIn())
router.Handle("/auth/signup", handler.handleSignUpPage()) router.Handle("/auth/signup", handler.handleSignUpPage())
router.Handle("/auth/verify", handler.handleSignUpVerifyPage()) router.Handle("/auth/verify", handler.handleSignUpVerifyPage())
@@ -40,17 +40,17 @@ func (handler AuthImpl) Handle(router *http.ServeMux) {
router.Handle("/auth/verify-email", handler.handleSignUpVerifyResponsePage()) router.Handle("/auth/verify-email", handler.handleSignUpVerifyResponsePage())
router.Handle("/api/auth/signup", handler.handleSignUp()) router.Handle("/api/auth/signup", handler.handleSignUp())
router.Handle("/api/auth/signout", handler.handleSignOut()) router.Handle("POST /api/auth/signout", handler.handleSignOut())
router.Handle("/auth/delete-account", handler.handleDeleteAccountPage()) router.Handle("/auth/delete-account", handler.handleDeleteAccountPage())
router.Handle("/api/auth/delete-account", handler.handleDeleteAccountComp()) router.Handle("/api/auth/delete-account", handler.handleDeleteAccountComp())
router.Handle("/auth/change-password", handler.handleChangePasswordPage()) router.Handle("GET /auth/change-password", handler.handleChangePasswordPage())
router.Handle("/api/auth/change-password", handler.handleChangePasswordComp()) router.Handle("POST /api/auth/change-password", handler.handleChangePasswordComp())
router.Handle("/auth/reset-password", handler.handleResetPasswordPage()) router.Handle("GET /auth/forgot-password", handler.handleForgotPasswordPage())
router.Handle("/api/auth/reset-password", handler.handleForgotPasswordComp()) router.Handle("POST /api/auth/forgot-password", handler.handleForgotPasswordComp())
router.Handle("/api/auth/reset-password-actual", handler.handleForgotPasswordResponseComp()) router.Handle("POST /api/auth/forgot-password-actual", handler.handleForgotPasswordResponseComp())
} }
var ( var (
@@ -77,11 +77,13 @@ func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
func (handler AuthImpl) handleSignIn() http.HandlerFunc { func (handler AuthImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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")
session, err := handler.service.SignIn(email, 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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -89,16 +91,14 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
cookie := middleware.CreateSessionCookie(session.Id) cookie := middleware.CreateSessionCookie(session.Id)
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
return session.User, nil return user, nil
}) })
if err != nil { if err != nil {
if err == service.ErrInvaidCredentials { if err == service.ErrInvalidCredentials {
utils.TriggerToast(w, r, "error", "Invalid email or password") utils.TriggerToast(w, r, "error", "Invalid email or password", http.StatusUnauthorized)
http.Error(w, "Invalid email or password", http.StatusUnauthorized)
} else { } else {
log.Error("Error signing in: %v", err) utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError)
http.Error(w, "An error occurred", http.StatusInternalServerError)
} }
return return
} }
@@ -171,11 +171,17 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
err := handler.service.VerifyUserEmail(token) err := handler.service.VerifyUserEmail(token)
if err != nil { isVerified := err == nil
utils.DoRedirect(w, r, "/auth/signin") comp := auth.VerifyResponseComp(isVerified)
var status int
if isVerified {
status = http.StatusOK
} else { } else {
utils.DoRedirect(w, r, "/") status = http.StatusBadRequest
} }
handler.render.RenderLayoutWithStatus(r, w, comp, nil, status)
} }
} }
@@ -198,16 +204,19 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc {
if err != nil { if err != nil {
if errors.Is(err, types.ErrInternal) { if errors.Is(err, types.ErrInternal) {
utils.TriggerToast(w, r, "error", "An error occurred") utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError)
return return
} else if errors.Is(err, service.ErrInvalidEmail) { } else if errors.Is(err, service.ErrInvalidEmail) {
utils.TriggerToast(w, r, "error", "The email provided is invalid") 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)
return return
} }
// If the "service.ErrAccountExists", then just continue // If err is "service.ErrAccountExists", then just continue
} }
utils.TriggerToast(w, r, "success", "A link to activate your account has been emailed to the address provided.") utils.TriggerToast(w, r, "success", "An activation link has been send to your email", http.StatusOK)
} }
} }
@@ -261,15 +270,13 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
password := r.FormValue("password") password := r.FormValue("password")
_, err := handler.service.SignIn(user.Email, password) err := handler.service.DeleteAccount(user, password)
if err != nil { if err != nil {
utils.TriggerToast(w, r, "error", "Password not correct") if err == service.ErrInvalidCredentials {
return utils.TriggerToast(w, r, "error", "Password not correct", http.StatusBadRequest)
} } else {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
err = handler.service.DeleteAccount(user) }
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error")
return return
} }
@@ -297,31 +304,32 @@ func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc { func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if session == nil || user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.TriggerToast(w, r, "error", "Unathorized", http.StatusUnauthorized)
return return
} }
currPass := r.FormValue("current-password") currPass := r.FormValue("current-password")
newPass := r.FormValue("new-password") newPass := r.FormValue("new-password")
err := handler.service.ChangePassword(user, currPass, newPass) err := handler.service.ChangePassword(user, session.Id, currPass, newPass)
if err != nil { if err != nil {
utils.TriggerToast(w, r, "error", "Password not correct") utils.TriggerToast(w, r, "error", "Password not correct", http.StatusBadRequest)
return return
} }
utils.TriggerToast(w, r, "success", "Password changed") utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK)
} }
} }
func (handler AuthImpl) handleResetPasswordPage() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user != nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/")
return return
} }
@@ -335,42 +343,40 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
email := r.FormValue("email") email := r.FormValue("email")
if email == "" { if email == "" {
utils.TriggerToast(w, r, "error", "Please enter an email") utils.TriggerToast(w, r, "error", "Please enter an email", http.StatusBadRequest)
return return
} }
err := handler.service.SendForgotPasswordMail(email) _, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) {
err := handler.service.SendForgotPasswordMail(email)
return nil, err
})
if err != nil { if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error") utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
} else { } else {
utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent") utils.TriggerToast(w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
} }
} }
} }
func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL")) pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL"))
if err != nil { if err != nil {
log.Error("Could not get current URL: %v", err) log.Error("Could not get current URL: %v", err)
utils.TriggerToast(w, r, "error", "Internal Server Error") utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
return return
} }
token := pageUrl.Query().Get("token") token := pageUrl.Query().Get("token")
if token == "" {
utils.TriggerToast(w, r, "error", "No token")
return
}
newPass := r.FormValue("new-password") newPass := r.FormValue("new-password")
err = handler.service.ForgotPassword(token, newPass) err = handler.service.ForgotPassword(token, newPass)
if err != nil { if err != nil {
utils.TriggerToast(w, r, "error", err.Error()) utils.TriggerToast(w, r, "error", err.Error(), http.StatusBadRequest)
} else { } else {
utils.TriggerToast(w, r, "success", "Password changed") utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK)
} }
} }
} }

View File

@@ -32,21 +32,19 @@ func (handler IndexImpl) Handle(router *http.ServeMux) {
func (handler IndexImpl) handleIndexAnd404() http.HandlerFunc { func (handler IndexImpl) handleIndexAnd404() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r) user := middleware.GetUser(r)
var user *service.User
if session != nil {
user = session.User
}
var comp templ.Component var comp templ.Component
var status int
if r.URL.Path != "/" { if r.URL.Path != "/" {
comp = template.NotFound() comp = template.NotFound()
w.WriteHeader(http.StatusNotFound) status = http.StatusNotFound
} else { } else {
comp = template.Index() comp = template.Index()
status = http.StatusOK
} }
handler.render.RenderLayout(r, w, comp, user) handler.render.RenderLayoutWithStatus(r, w, comp, user, status)
} }
} }

View File

@@ -2,50 +2,63 @@ package middleware
import ( import (
"context" "context"
"net/http"
"me-fit/service" "me-fit/service"
"me-fit/types"
"net/http"
) )
type ContextKey string type ContextKey string
var SessionKey ContextKey = "session" var SessionKey ContextKey = "session"
var UserKey ContextKey = "user"
func Authenticate(service service.Auth) func(http.Handler) http.Handler { func Authenticate(service service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sessionId := getSessionID(r) sessionId := getSessionID(r)
session, _ := service.SignInSession(sessionId) session, user, _ := service.SignInSession(sessionId)
if session != nil { var err error
ctx := context.WithValue(r.Context(), SessionKey, session) // 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
}
next.ServeHTTP(w, r.WithContext(ctx)) cookie := CreateSessionCookie(session.Id)
} else { http.SetCookie(w, &cookie)
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) *service.User { func GetUser(r *http.Request) *types.User {
obj := r.Context().Value(UserKey)
session := GetSession(r) if obj == nil {
if session == nil {
return nil return nil
} }
return session.User return obj.(*types.User)
} }
func GetSession(r *http.Request) *service.Session { func GetSession(r *http.Request) *types.Session {
obj := r.Context().Value(SessionKey) obj := r.Context().Value(SessionKey)
if obj == nil { if obj == nil {
return nil return nil
} }
return obj.(*service.Session) return obj.(*types.Session)
} }
func getSessionID(r *http.Request) string { func getSessionID(r *http.Request) string {

View File

@@ -0,0 +1,23 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,23 +0,0 @@
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,20 +2,22 @@ package middleware
import ( import (
"fmt" "fmt"
"net/http"
"strings" "strings"
"me-fit/log"
"me-fit/service" "me-fit/service"
"me-fit/types"
"net/http" "me-fit/utils"
) )
type csrfResponseWriter struct { type csrfResponseWriter struct {
http.ResponseWriter http.ResponseWriter
auth service.Auth auth service.Auth
session *service.Session session *types.Session
} }
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *service.Session) *csrfResponseWriter { func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *types.Session) *csrfResponseWriter {
return &csrfResponseWriter{ return &csrfResponseWriter{
ResponseWriter: w, ResponseWriter: w,
auth: auth, auth: auth,
@@ -25,13 +27,11 @@ func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *se
func (rr *csrfResponseWriter) Write(data []byte) (int, error) { func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
dataStr := string(data) dataStr := string(data)
if strings.Contains(dataStr, "</form>") { csrfToken, err := rr.auth.GetCsrfToken(rr.session)
csrfToken, err := rr.auth.GetCsrfToken(rr.session) if err == nil {
if err == nil { csrfInput := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s" />`, csrfToken)
csrfField := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s">`, csrfToken) dataStr = strings.ReplaceAll(dataStr, "</form>", csrfInput+"</form>")
dataStr = strings.ReplaceAll(dataStr, "</form>", csrfField+"</form>") dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
}
} }
return rr.ResponseWriter.Write([]byte(dataStr)) return rr.ResponseWriter.Write([]byte(dataStr))
@@ -56,19 +56,17 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
if csrfToken == "" { if csrfToken == "" {
csrfToken = r.Header.Get("csrf-token") csrfToken = r.Header.Get("csrf-token")
} }
if csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) { if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
http.Error(w, "", http.StatusForbidden) 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)
}
return return
} }
} }
if session == nil && (strings.Contains(r.RequestURI, "/auth/signup") || strings.Contains(r.RequestURI, "/auth/signin")) {
session, _ = auth.SignInAnonymous()
cookie := CreateSessionCookie(session.Id)
http.SetCookie(w, &cookie)
}
responseWriter := newCsrfResponseWriter(w, auth, session) responseWriter := newCsrfResponseWriter(w, auth, session)
next.ServeHTTP(responseWriter, r) next.ServeHTTP(responseWriter, r)
}) })

View File

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

View File

@@ -0,0 +1,40 @@
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,7 +2,6 @@ package handler
import ( import (
"me-fit/log" "me-fit/log"
"me-fit/service"
"me-fit/template" "me-fit/template"
"me-fit/template/auth" "me-fit/template/auth"
"me-fit/types" "me-fit/types"
@@ -22,7 +21,9 @@ func NewRender(settings *types.Settings) *Render {
} }
} }
func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.Component) { 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)
err := comp.Render(r.Context(), w) err := comp.Render(r.Context(), w)
if err != nil { if err != nil {
log.Error("Failed to render layout: %v", err) log.Error("Failed to render layout: %v", err)
@@ -30,14 +31,22 @@ func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.
} }
} }
func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *service.User) { 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) {
userComp := render.getUserComp(user) userComp := render.getUserComp(user)
layout := template.Layout(slot, userComp, render.settings.Environment) layout := template.Layout(slot, userComp, render.settings.Environment)
render.Render(r, w, layout) render.RenderWithStatus(r, w, layout, status)
} }
func (render *Render) getUserComp(user *service.User) templ.Component { func (render *Render) getUserComp(user *types.User) templ.Component {
if user != nil { if user != nil {
return auth.UserComp(user.Email) return auth.UserComp(user.Email)

View File

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

28
less
View File

@@ -1,28 +0,0 @@
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ v1.52.3, built with Go go1.22.5
mkdir /home/tiwun/source/me-fit/tmp
watching .
watching db
watching handler
watching handler/middleware
watching log
watching migration
watching mocks
!exclude node_modules
watching service
!exclude static
watching template
watching template/auth
watching template/mail
watching template/workout
!exclude tmp
watching types
watching utils
building...
(✓) Complete [ updates=12 duration=10.258748ms ]
cleaning...
deleting /home/tiwun/source/me-fit/tmp
see you again~

View File

@@ -128,11 +128,9 @@ func createHandler(d *sql.DB, serverSettings *types.Settings) http.Handler {
return middleware.Wrapper( return middleware.Wrapper(
router, router,
middleware.Log, middleware.Log,
middleware.ContentSecurityPolicy, middleware.CacheControl,
middleware.Cors(serverSettings), middleware.SecurityHeaders(serverSettings),
middleware.Authenticate(authService), middleware.Authenticate(authService),
middleware.CrossSiteRequestForgery(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", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"daisyui": "4.12.14", "daisyui": "4.12.22",
"htmx.org": "2.0.3", "htmx.org": "2.0.4",
"tailwindcss": "3.4.16" "tailwindcss": "3.4.17"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -344,9 +344,9 @@
} }
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "4.12.14", "version": "4.12.22",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.14.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.22.tgz",
"integrity": "sha512-hA27cdBasdwd4/iEjn+aidoCrRroDuo3G5W9NDKaVCJI437Mm/3eSL/2u7MkZ0pt8a+TrYF3aT2pFVemTS3how==", "integrity": "sha512-HDLWbmTnXxhE1MrMgSWjVgdRt+bVYHvfNbW3GTsyIokRSqTHonUTrxV3RhpPDjGIWaHt+ELtDCTYCtUFgL2/Nw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -527,9 +527,9 @@
} }
}, },
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "2.0.3", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
"integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA==", "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==",
"dev": true, "dev": true,
"license": "0BSD" "license": "0BSD"
}, },
@@ -1240,9 +1240,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.16", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import (
"me-fit/mocks" "me-fit/mocks"
"me-fit/types" "me-fit/types"
"errors"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -15,104 +14,6 @@ import (
"github.com/stretchr/testify/mock" "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) { func TestSignUp(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("should check for correct email address", func(t *testing.T) { t.Run("should check for correct email address", func(t *testing.T) {
@@ -159,33 +60,25 @@ func TestSignUp(t *testing.T) {
mockClock := mocks.NewMockClock(t) mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t) mockMail := mocks.NewMockMail(t)
expected := User{ userId := uuid.New()
Id: uuid.New(), email := "mail@mail.de"
Email: "some@valid.email",
EmailVerified: false,
}
random := NewRandomImpl()
salt, err := random.Bytes(16)
assert.Nil(t, err)
password := "SomeStrongPassword123!" 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) createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
mockClock.EXPECT().Now().Return(createTime) expected := types.NewUser(userId, email, false, nil, false, GetHashPassword(password, salt), salt, createTime)
mockAuthDb.EXPECT().InsertUser(db.NewUser(expected.Id, expected.Email, false, nil, false, GetHashPassword(password, salt), salt, createTime)).Return(nil) mockRandom.EXPECT().UUID().Return(userId, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(expected).Return(nil)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{}) 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.Nil(t, err)
assert.Equal(t, expected, *actual) assert.Equal(t, expected, actual)
}) })
t.Run("should return ErrAccountExists", func(t *testing.T) { t.Run("should return ErrAccountExists", func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -195,28 +88,22 @@ func TestSignUp(t *testing.T) {
mockClock := mocks.NewMockClock(t) mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t) mockMail := mocks.NewMockMail(t)
user := User{ userId := uuid.New()
Id: uuid.New(), email := "some@valid.email"
Email: "some@valid.email", createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
}
random := NewRandomImpl()
salt, err := random.Bytes(16)
assert.Nil(t, err)
password := "SomeStrongPassword123!" 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().UUID().Return(user.Id, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, 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) mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(db.NewUser(user.Id, user.Email, false, nil, false, GetHashPassword(password, salt), salt, createTime)).Return(db.ErrAlreadyExists) mockAuthDb.EXPECT().InsertUser(user).Return(db.ErrAlreadyExists)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{}) 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) assert.Equal(t, ErrAccountExists, err)
}) })
} }
@@ -227,8 +114,8 @@ func TestSendVerificationMail(t *testing.T) {
t.Run("should use stored token and send mail", func(t *testing.T) { t.Run("should use stored token and send mail", func(t *testing.T) {
t.Parallel() t.Parallel()
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)) 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 := []*db.Token{token} tokens := []*types.Token{token}
email := "some@email.de" email := "some@email.de"
userId := uuid.New() userId := uuid.New()
@@ -238,7 +125,7 @@ func TestSendVerificationMail(t *testing.T) {
mockClock := mocks.NewMockClock(t) mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t) mockMail := mocks.NewMockMail(t)
mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, db.TokenTypeEmailVerify).Return(tokens, nil) mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify).Return(tokens, nil)
mockMail.EXPECT().SendMail(email, "Welcome to ME-FIT", mock.MatchedBy(func(message string) bool { mockMail.EXPECT().SendMail(email, "Welcome to ME-FIT", mock.MatchedBy(func(message string) bool {
return strings.Contains(message, token.Token) return strings.Contains(message, token.Token)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,18 @@
package auth package auth
templ SignInOrUpComp(isSignIn bool) { templ SignInOrUpComp(isSignIn bool) {
{{
var postUrl string
if isSignIn {
postUrl = "/api/auth/signin"
} else {
postUrl = "/api/auth/signup"
}
}}
<form <form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center" 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-target="#sign-in-or-up-error"
if isSignIn { hx-post={ postUrl }
hx-post="/api/auth/signin"
} else {
hx-post="/api/auth/signup"
}
> >
<h2 class="text-6xl mb-10"> <h2 class="text-6xl mb-10">
if isSignIn { if isSignIn {
@@ -18,12 +22,7 @@ templ SignInOrUpComp(isSignIn bool) {
} }
</h2> </h2>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex items-center gap-2">
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-4 w-4 opacity-70">
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path <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" 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> ></path>
@@ -31,26 +30,39 @@ templ SignInOrUpComp(isSignIn bool) {
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" 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> ></path>
</svg> </svg>
<input type="text" class="grow" placeholder="Email" name="email"/> <input
type="text"
class="grow"
placeholder="Email"
name="email"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label> </label>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex items-center gap-2">
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-4 w-4 opacity-70">
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path <path
fill-rule="evenodd" 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" 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" clip-rule="evenodd"
></path> ></path>
</svg> </svg>
<input type="password" class="grow" placeholder="Password" name="password"/> <input
type="password"
class="grow"
placeholder="Password"
name="password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label> </label>
<div class="flex justify-end items-center gap-2"> <div class="flex justify-end items-center gap-2">
if isSignIn { if isSignIn {
<a href="/auth/reset-password" class="grow link text-gray-500 text-sm">Forgot Password?</a> <a href="/auth/forgot-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> <a href="/auth/signup" class="link text-gray-500 text-sm">Don't have an account? Sign Up</a>
<button class="btn btn-primary"> <button class="btn btn-primary">
Sign In Sign In

View File

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

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

View File

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

View File

@@ -39,14 +39,12 @@ templ WorkoutListComp(workouts []Workout) {
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<form> <tbody>
<tbody> <tr class="hidden" id="workout-placeholder"></tr>
<tr class="hidden" id="workout-placeholder"></tr> for _,w := range workouts {
for _,w := range workouts { @WorkoutItemComp(w, false)
@WorkoutItemComp(w, false) }
} </tbody>
</tbody>
</form>
</table> </table>
</div> </div>
} }
@@ -62,8 +60,7 @@ if includePlaceholder {
<th>{ w.Reps }</th> <th>{ w.Reps }</th>
<th> <th>
<div class="tooltip" data-tip="Delete Entry"> <div class="tooltip" data-tip="Delete Entry">
<button hx-headers='{"csrf-token": "CSRF_TOKEN"}' hx-delete={ "api/workout/" + w.Id } hx-target="closest tr" <button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr" type="submit">
type="submit">
Delete Delete
</button> </button>
</div> </div>

75
types/auth.go Normal file
View File

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