Add user repo and keycloak
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"ersteller-lib"
|
"ersteller-lib"
|
||||||
|
"ersteller-lib/authentication"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -28,22 +29,17 @@ type AuthEnv struct {
|
|||||||
SessionName string
|
SessionName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRepository interface {
|
|
||||||
GetUserId(email string) (int, error)
|
|
||||||
Create(email string) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Config oauth2.Config
|
Config oauth2.Config
|
||||||
repo *GoogleAuthRepository
|
repo *GoogleAuthRepository
|
||||||
isLocal bool
|
isLocal bool
|
||||||
userRepo UserRepository
|
userRepo authentication.UserRepository
|
||||||
sessionStore *sessions.CookieStore
|
sessionStore *sessions.CookieStore
|
||||||
GoogleLoginRoute ersteller_lib.Route
|
GoogleLoginRoute ersteller_lib.Route
|
||||||
environment AuthEnv
|
environment AuthEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuth(env AuthEnv, repo *GoogleAuthRepository, userRepo UserRepository, sessionStore *sessions.CookieStore) *Auth {
|
func NewAuth(env AuthEnv, repo *GoogleAuthRepository, userRepo authentication.UserRepository, sessionStore *sessions.CookieStore) *Auth {
|
||||||
config := oauth2.Config{
|
config := oauth2.Config{
|
||||||
ClientID: env.GoogleClientId,
|
ClientID: env.GoogleClientId,
|
||||||
ClientSecret: env.GoogleClientSecret,
|
ClientSecret: env.GoogleClientSecret,
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package authentication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"ersteller-lib"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/markbates/goth"
|
||||||
|
"github.com/markbates/goth/gothic"
|
||||||
|
"github.com/markbates/goth/providers/openidConnect"
|
||||||
|
)
|
||||||
|
|
||||||
|
const OpenIdConnectPath = "/auth/openid-connect"
|
||||||
|
|
||||||
|
type KeycloakEnv struct {
|
||||||
|
SessionSecret string
|
||||||
|
BaseUrl string
|
||||||
|
SessionName string
|
||||||
|
Keycloak struct {
|
||||||
|
CLientId string
|
||||||
|
ClientSecret string
|
||||||
|
DiscoveryUrl string
|
||||||
|
}
|
||||||
|
EmailSessionKey string
|
||||||
|
UserIdSessionKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://keycloak-dev.deploy.ersteller.gorlug.de/realms/master/.well-known/openid-configuration
|
||||||
|
// https://go.dev/play/p/-RtLSPL4Wsj
|
||||||
|
func RunKeycloakAuth(e *echo.Echo, environment KeycloakEnv, cookieStore *sessions.CookieStore, userRepo UserRepository) {
|
||||||
|
sessionStore := sessions.NewFilesystemStore("store", []byte(environment.SessionSecret))
|
||||||
|
sessionStore.MaxLength(8192)
|
||||||
|
gothic.Store = sessionStore
|
||||||
|
// OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html)
|
||||||
|
// because the OpenID Connect provider initialize itself in the New(), it can return an error which should be handled or ignored
|
||||||
|
// ignore the error for now
|
||||||
|
openid, err := openidConnect.New(environment.Keycloak.CLientId, environment.Keycloak.ClientSecret, environment.BaseUrl+"/auth/openid-connect/callback", environment.Keycloak.DiscoveryUrl)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.Error("Error while initializing OpenID Connect provider: ", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if openid != nil {
|
||||||
|
goth.UseProviders(openid)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.GET("/auth/openid-connect/callback", func(c echo.Context) error {
|
||||||
|
|
||||||
|
user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userId, err := userRepo.GetUserId(user.Email)
|
||||||
|
if err != nil {
|
||||||
|
if userId == -1 {
|
||||||
|
userId, err = createUser(user, userRepo)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("Failed to create user: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ersteller_lib.LogDebug("Created user with id %d", userId)
|
||||||
|
} else {
|
||||||
|
ersteller_lib.LogError("Failed to get user id: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = saveEmailToSessionStore(c, cookieStore, user.Email, userId, environment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Redirect(302, "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET("/logout", func(c echo.Context) error {
|
||||||
|
// Get the session
|
||||||
|
session, err := cookieStore.Get(c.Request(), environment.SessionName)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("Failed to get session during logout: %v", err)
|
||||||
|
} else {
|
||||||
|
// Clear session values
|
||||||
|
session.Values = make(map[interface{}]interface{})
|
||||||
|
// Set MaxAge to -1 to delete the cookie
|
||||||
|
session.Options.MaxAge = -1
|
||||||
|
// Save the session (this will delete the cookie)
|
||||||
|
err = session.Save(c.Request(), c.Response())
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("Failed to clear session during logout: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also call gothic logout for OpenID Connect cleanup
|
||||||
|
gothic.Logout(c.Response(), c.Request())
|
||||||
|
|
||||||
|
// Redirect to login page
|
||||||
|
return c.Redirect(302, "/login")
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET("/logout/openid-connect", func(c echo.Context) error {
|
||||||
|
return gothic.Logout(c.Response(), c.Request())
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET(OpenIdConnectPath, func(c echo.Context) error {
|
||||||
|
ctx := context.WithValue(c.Request().Context(), gothic.ProviderParamKey, "openid-connect")
|
||||||
|
request := c.Request().WithContext(ctx)
|
||||||
|
// try to get the user without re-authenticating
|
||||||
|
if gothUser, err := gothic.CompleteUserAuth(c.Response(), c.Request()); err == nil {
|
||||||
|
ersteller_lib.Debug(gothUser)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
gothic.BeginAuthHandler(c.Response(), request)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUser(gothUser goth.User, repo UserRepository) (int, error) {
|
||||||
|
return repo.Create(gothUser.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveEmailToSessionStore(c echo.Context, sessionStore *sessions.CookieStore, email string, userId int, environment KeycloakEnv) error {
|
||||||
|
session, err := sessionStore.New(c.Request(), environment.SessionName)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("Failed to create session: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session.Values = map[interface{}]interface{}{
|
||||||
|
environment.EmailSessionKey: email,
|
||||||
|
environment.UserIdSessionKey: userId,
|
||||||
|
}
|
||||||
|
err = session.Save(c.Request(), c.Response())
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("Failed to save session: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package authentication
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
GetUserId(email string) (int, error)
|
||||||
|
Create(email string) (int, error)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/jackc/pgx/v5 v5.7.5
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
|
github.com/markbates/goth v1.81.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.29
|
github.com/mattn/go-sqlite3 v1.14.29
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.40.0
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
@@ -16,6 +17,9 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||||
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
|
github.com/gorilla/mux v1.6.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
github.com/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSvBx7qo=
|
github.com/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSvBx7qo=
|
||||||
github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
@@ -30,6 +36,8 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0
|
|||||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/markbates/goth v1.81.0 h1:XVcCkeGWokynPV7MXvgb8pd2s3r7DS40P7931w6kdnE=
|
||||||
|
github.com/markbates/goth v1.81.0/go.mod h1:+6z31QyUms84EHmuBY7iuqYSxyoN3njIgg9iCF/lR1k=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -39,8 +47,9 @@ github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFa
|
|||||||
github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserState struct {
|
||||||
|
SomeValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id int `db:"id"`
|
||||||
|
Email string `db:"email"`
|
||||||
|
State UserState `db:"state"`
|
||||||
|
Admin bool `db:"admin"`
|
||||||
|
Password string `db:"password"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s User) String() string {
|
||||||
|
return fmt.Sprint("User{ ",
|
||||||
|
"Id: ", s.Id, ", ",
|
||||||
|
"Email: ", s.Email, ", ",
|
||||||
|
"State: ", s.State, ", ",
|
||||||
|
"Admin: ", s.Admin, ", ",
|
||||||
|
"Password: ", s.Password, ", ",
|
||||||
|
"CreatedAt: ", s.CreatedAt, ", ",
|
||||||
|
"UpdatedAt: ", s.UpdatedAt, ", ",
|
||||||
|
"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s User) GetId() int {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"ersteller-lib"
|
||||||
|
"fmt"
|
||||||
|
"github.com/doug-martin/goqu/v9"
|
||||||
|
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
|
||||||
|
"github.com/doug-martin/goqu/v9/exp"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository struct {
|
||||||
|
connPool *pgxpool.Pool
|
||||||
|
dialect goqu.DialectWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserRepository(connPool *pgxpool.Pool) *UserRepository {
|
||||||
|
return &UserRepository{
|
||||||
|
connPool: connPool,
|
||||||
|
dialect: goqu.Dialect("postgres"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Create(user User) (int, error) {
|
||||||
|
sql, args, err := r.dialect.Insert("user").
|
||||||
|
Prepared(true).
|
||||||
|
Rows(goqu.Record{
|
||||||
|
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
"email": user.Email,
|
||||||
|
"state": r.jsonToString(user.State),
|
||||||
|
"admin": user.Admin,
|
||||||
|
"password": user.Password,
|
||||||
|
}).
|
||||||
|
Returning("id").
|
||||||
|
ToSQL()
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error creating create User sql: %v", err)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.connPool.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error creating User: %v", err)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var id int
|
||||||
|
if rows.Next() {
|
||||||
|
err = rows.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error scanning User: %v", err)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ersteller_lib.Error("User already exists")
|
||||||
|
return -1, UserAlreadyExistsError{User: user}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAlreadyExistsError struct {
|
||||||
|
User User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserAlreadyExistsError) Error() string {
|
||||||
|
return fmt.Sprint("User ", e.User, " already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) getSelectColumns() []any {
|
||||||
|
return []any{"id", "created_at", "updated_at",
|
||||||
|
"email", "state", "admin", "password",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Read(id int) (User, error) {
|
||||||
|
ersteller_lib.Debug("Getting User by id ", id)
|
||||||
|
sql, args, _ := r.dialect.From("user").
|
||||||
|
Prepared(true).
|
||||||
|
Select(r.getSelectColumns()...).
|
||||||
|
Where(goqu.Ex{
|
||||||
|
"id": id,
|
||||||
|
}).
|
||||||
|
ToSQL()
|
||||||
|
|
||||||
|
rows, err := r.connPool.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.Error("Failed to get User: ", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
item, _, err := r.rowToItem(rows, false)
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
return User{}, errors.New("no rows found")
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserItemScan struct {
|
||||||
|
User
|
||||||
|
RowId int
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) rowToItem(rows pgx.Rows, rowId bool) (User, int, error) {
|
||||||
|
var item UserItemScan
|
||||||
|
if rowId {
|
||||||
|
err := rows.Scan(
|
||||||
|
&item.RowId,
|
||||||
|
&item.Count,
|
||||||
|
&item.Id,
|
||||||
|
&item.CreatedAt,
|
||||||
|
&item.UpdatedAt,
|
||||||
|
&item.Email,
|
||||||
|
&item.State,
|
||||||
|
&item.Admin,
|
||||||
|
&item.Password,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, -1, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := rows.Scan(
|
||||||
|
&item.Id,
|
||||||
|
&item.CreatedAt,
|
||||||
|
&item.UpdatedAt,
|
||||||
|
&item.Email,
|
||||||
|
&item.State,
|
||||||
|
&item.Admin,
|
||||||
|
&item.Password,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return User{
|
||||||
|
Id: item.Id,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
|
Email: item.Email,
|
||||||
|
State: item.State,
|
||||||
|
Admin: item.Admin,
|
||||||
|
Password: item.Password,
|
||||||
|
}, item.Count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Update(user User) error {
|
||||||
|
sql, args, err := r.dialect.Update("user").
|
||||||
|
Prepared(true).
|
||||||
|
Set(goqu.Record{
|
||||||
|
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
"email": user.Email,
|
||||||
|
"state": r.jsonToString(user.State),
|
||||||
|
"admin": user.Admin,
|
||||||
|
"password": user.Password,
|
||||||
|
}).
|
||||||
|
Where(goqu.Ex{
|
||||||
|
"id": user.Id,
|
||||||
|
}).
|
||||||
|
ToSQL()
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error creating update User sql: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.connPool.Exec(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error updating User: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Delete(id int) error {
|
||||||
|
sql, args, err := r.dialect.Delete("user").
|
||||||
|
Prepared(true).
|
||||||
|
Where(goqu.Ex{
|
||||||
|
"id": id,
|
||||||
|
}).
|
||||||
|
ToSQL()
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error creating delete User sql: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.connPool.Exec(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("error deleting User: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserFieldEmail UserField = "email"
|
||||||
|
UserFieldState UserField = "state"
|
||||||
|
UserFieldAdmin UserField = "admin"
|
||||||
|
UserFieldPassword UserField = "password"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserEmailFilter struct {
|
||||||
|
Active bool
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAdminFilter struct {
|
||||||
|
Active bool
|
||||||
|
Value bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserPasswordFilter struct {
|
||||||
|
Active bool
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserOrderDirection string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserOrderDirectionAsc UserOrderDirection = "asc"
|
||||||
|
UserOrderDirectionDesc UserOrderDirection = "desc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserPaginationParams struct {
|
||||||
|
RowId int
|
||||||
|
PageSize int
|
||||||
|
OrderBy UserField
|
||||||
|
OrderDirection UserOrderDirection
|
||||||
|
|
||||||
|
EmailFilter UserEmailFilter
|
||||||
|
|
||||||
|
AdminFilter UserAdminFilter
|
||||||
|
|
||||||
|
PasswordFilter UserPasswordFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetPage(params UserPaginationParams) ([]User, int, error) {
|
||||||
|
var orderByWindow exp.WindowExpression
|
||||||
|
if params.OrderDirection == UserOrderDirectionAsc {
|
||||||
|
orderByWindow = goqu.W().OrderBy(goqu.C(string(params.OrderBy)).Asc())
|
||||||
|
} else {
|
||||||
|
orderByWindow = goqu.W().OrderBy(goqu.C(string(params.OrderBy)).Desc())
|
||||||
|
}
|
||||||
|
selectColumns := []any{
|
||||||
|
goqu.ROW_NUMBER().Over(orderByWindow).As("row_id"),
|
||||||
|
goqu.COUNT("*"),
|
||||||
|
}
|
||||||
|
selectColumns = append(selectColumns, r.getSelectColumns()...)
|
||||||
|
whereExpressions := []goqu.Expression{
|
||||||
|
goqu.Ex{},
|
||||||
|
}
|
||||||
|
whereExpressions = r.addPageFilters(params, whereExpressions)
|
||||||
|
var colOrder exp.OrderedExpression
|
||||||
|
if params.OrderDirection == UserOrderDirectionAsc {
|
||||||
|
colOrder = goqu.C(string(params.OrderBy)).Asc()
|
||||||
|
} else {
|
||||||
|
colOrder = goqu.C(string(params.OrderBy)).Desc()
|
||||||
|
}
|
||||||
|
dialect := goqu.Dialect("postgres")
|
||||||
|
innerFrom := dialect.From("user").
|
||||||
|
Prepared(true).
|
||||||
|
Select(selectColumns...).
|
||||||
|
Where(whereExpressions...).
|
||||||
|
Order(colOrder)
|
||||||
|
|
||||||
|
outerFrom := dialect.From(innerFrom).
|
||||||
|
Prepared(true).
|
||||||
|
Where(goqu.Ex{"row_id": goqu.Op{"gt": params.RowId}})
|
||||||
|
if params.PageSize > 0 {
|
||||||
|
outerFrom = outerFrom.Limit(uint(params.PageSize))
|
||||||
|
}
|
||||||
|
sql, args, _ := outerFrom.ToSQL()
|
||||||
|
sql = strings.Replace(sql, "COUNT(*)", "COUNT(*) over()", 1)
|
||||||
|
|
||||||
|
rows, err := r.connPool.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("failed to run sql query: %v", err)
|
||||||
|
return nil, -1, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
results := make([]User, 0)
|
||||||
|
totalCount := 0
|
||||||
|
for rows.Next() {
|
||||||
|
parsed, count, err := r.rowToItem(rows, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, -1, err
|
||||||
|
}
|
||||||
|
totalCount = count
|
||||||
|
results = append(results, parsed)
|
||||||
|
}
|
||||||
|
return results, totalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) addPageFilters(params UserPaginationParams, whereExpressions []goqu.Expression) []goqu.Expression {
|
||||||
|
|
||||||
|
if params.EmailFilter.Active {
|
||||||
|
whereExpressions = append(whereExpressions, goqu.Ex{
|
||||||
|
"email": goqu.Op{"ilike": fmt.Sprint("%", params.EmailFilter.Value, "%")},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.AdminFilter.Active {
|
||||||
|
whereExpressions = append(whereExpressions, goqu.Ex{
|
||||||
|
"admin": params.AdminFilter.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.PasswordFilter.Active {
|
||||||
|
whereExpressions = append(whereExpressions, goqu.Ex{
|
||||||
|
"password": goqu.Op{"ilike": fmt.Sprint("%", params.PasswordFilter.Value, "%")},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return whereExpressions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) jsonToString(jsonData any) string {
|
||||||
|
bytes, err := json.Marshal(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserRepository) DoesUserEmailExist(email string) (bool, error) {
|
||||||
|
sql, args, _ := u.dialect.From("user").
|
||||||
|
Prepared(true).
|
||||||
|
Select(goqu.COUNT("email")).
|
||||||
|
Where(goqu.Ex{"email": email}).
|
||||||
|
ToSQL()
|
||||||
|
|
||||||
|
rows, err := u.connPool.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("failed to run sql query: %v", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
var count int
|
||||||
|
err = rows.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count == 1, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserRepository) GetUserId(email string) (int, error) {
|
||||||
|
sql, args, _ := u.dialect.From("user").
|
||||||
|
Prepared(true).
|
||||||
|
Select("id").
|
||||||
|
Where(goqu.Ex{"email": email}).
|
||||||
|
ToSQL()
|
||||||
|
|
||||||
|
rows, err := u.connPool.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
ersteller_lib.LogError("failed to run sql query: %v", err)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
var id int
|
||||||
|
err = rows.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
return -1, errors.New("did not find user with email " + email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) VerifyPassword(email string, password string) (bool, int, error) {
|
||||||
|
userId, err := r.GetUserId(email)
|
||||||
|
if err != nil {
|
||||||
|
return false, -1, err
|
||||||
|
}
|
||||||
|
user, err := r.Read(userId)
|
||||||
|
if err != nil {
|
||||||
|
return false, -1, err
|
||||||
|
}
|
||||||
|
return ersteller_lib.VerifyPassword(password, user.Password), userId, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user