diff --git a/authentication/google/auth.go b/authentication/google/auth.go new file mode 100644 index 0000000..20d5912 --- /dev/null +++ b/authentication/google/auth.go @@ -0,0 +1,253 @@ +package google + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "ersteller-lib" + "github.com/gorilla/sessions" + "github.com/labstack/echo/v4" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "io/ioutil" + "net/http" + "time" +) + +const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + +type AuthEnv struct { + GoogleClientId string + GoogleClientSecret string + GoogleRedirectUrl string + IsLocal bool + EmailSessionKey string + UserIdSessionKey string + SessionName string +} + +type UserRepository interface { + GetUserId(email string) (int, error) + Create(email string) (int, error) +} + +type Auth struct { + Config oauth2.Config + repo *GoogleAuthRepository + isLocal bool + userRepo UserRepository + sessionStore *sessions.CookieStore + GoogleLoginRoute ersteller_lib.Route + environment AuthEnv +} + +func NewAuth(env AuthEnv, repo *GoogleAuthRepository, userRepo UserRepository, sessionStore *sessions.CookieStore) *Auth { + config := oauth2.Config{ + ClientID: env.GoogleClientId, + ClientSecret: env.GoogleClientSecret, + Endpoint: google.Endpoint, + RedirectURL: env.GoogleRedirectUrl, + Scopes: []string{"https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/userinfo.email"}, + } + return &Auth{ + repo: repo, + Config: config, + isLocal: env.IsLocal, + userRepo: userRepo, + sessionStore: sessionStore, + environment: env, + } +} + +func (a *Auth) GetAuthURL(state string) string { + return a.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) +} + +func (a *Auth) ParseUserData(code string) (GoogleUserData, error) { + data, err := a.getUserDataFromGoogle(code) + if err != nil { + ersteller_lib.LogError("failed getting user data from Google: %v", err) + return GoogleUserData{}, err + } + ersteller_lib.LogDebug("user data: %v", data) + return data, nil +} + +func (a *Auth) getUserDataFromGoogle(code string) (GoogleUserData, error) { + // Use code to get token and get user info from Google. + token, err := a.Config.Exchange(context.Background(), code) + if err != nil { + ersteller_lib.LogError("code exchange wrong: %v", err) + return GoogleUserData{}, err + } + + response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken) + if err != nil { + ersteller_lib.LogError("failed getting user info: %v", err) + return GoogleUserData{}, nil + } + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + ersteller_lib.LogError("failed read response: %v", err) + return GoogleUserData{}, nil + } + userData := GoogleUserData{} + err = json.Unmarshal(contents, &userData) + if err != nil { + ersteller_lib.LogError("failed to unmarshal user data: %v", err) + return GoogleUserData{}, nil + } + userData.Token = token + + return userData, nil +} + +func (a *Auth) SaveCredentials(userId int, token *oauth2.Token) error { + ersteller_lib.Debug("saving google credentials for user ", userId) + googleAuth, err := a.repo.ReadByUserId(userId) + if err != nil { + ersteller_lib.LogDebug("no GoogleAuth found for user %d, creating new one", userId) + _, err = a.repo.Create(GoogleAuth{ + UserId: userId, + Credentials: Credentials{ + Token: *token, + }, + }) + return err + } + googleAuth.Credentials.Token = *token + return a.repo.Update(userId, googleAuth) +} + +func (a *Auth) GetCredentials(userId int) (*oauth2.Token, error) { + credentials, err := a.repo.ReadByUserId(userId) + if err != nil { + ersteller_lib.LogError("failed to get credentials for user %d: %v", userId, err) + return nil, err + } + if credentials.Credentials.Token.AccessToken == "" { + ersteller_lib.LogError("no credentials found for user %d", userId) + return nil, errors.New("no credentials found") + } + return &credentials.Credentials.Token, nil +} + +const oAuthStateCookieName = "oauthstate" +const GoogleLogin = "/login/google" +const GoogleLoginCallback = "/email/authenticated" + +func (a *Auth) AddRoutes(e *echo.Echo) []ersteller_lib.Route { + googleLoginRoute := ersteller_lib.NewGetRoute(GoogleLogin, func(c echo.Context) error { + state := a.generateStateOauthCookie(c.Response()) + ersteller_lib.LogDebug("Value: %v", state) + authenticationUrl := a.GetAuthURL(state) + return c.Redirect(http.StatusTemporaryRedirect, authenticationUrl) + }) + googleLoginRoute.Add(e) + a.GoogleLoginRoute = googleLoginRoute + + authenticatedRoute := ersteller_lib.NewGetRoute(GoogleLoginCallback, func(c echo.Context) error { + ersteller_lib.LogDebug("email authenticated called") + oauthstate, err := c.Cookie(oAuthStateCookieName) + if err != nil { + ersteller_lib.LogError("Failed to get cookie: %v", err) + return err + } + if c.FormValue("state") != oauthstate.Value { + ersteller_lib.LogError("Failed to verify google oauth state") + return err + } + code := c.FormValue("code") + data, err := a.ParseUserData(code) + if err != nil { + return err + } + userId, err := a.userRepo.GetUserId(data.Email) + if err != nil { + ersteller_lib.LogError("Failed to get user id: %v", err) + userId, err = a.userRepo.Create(data.Email) + if err != nil { + ersteller_lib.LogError("Failed to create user: %v", err) + return err + } + } + err = a.saveEmailToSessionStore(c, data.Email, userId) + if err != nil { + return err + } + err = a.SaveCredentials(userId, data.Token) + if err != nil { + ersteller_lib.Error("failed to save credentials for user ", userId, ": ", err) + return err + } + ersteller_lib.Debug("saved credentials for user", userId) + return c.Redirect(http.StatusTemporaryRedirect, "/") + }) + authenticatedRoute.Add(e) + + logoutRoute := ersteller_lib.NewGetRoute("/logout", func(c echo.Context) error { + // Clear the session + session, err := a.sessionStore.Get(c.Request(), a.environment.SessionName) + if err != nil { + ersteller_lib.LogError("Failed to get session: %v", err) + return c.Redirect(http.StatusTemporaryRedirect, "/") + } + session.Options.MaxAge = -1 + err = session.Save(c.Request(), c.Response()) + if err != nil { + ersteller_lib.LogError("Failed to save session: %v", err) + return err + } + return c.Redirect(http.StatusTemporaryRedirect, "/") + }) + logoutRoute.Add(e) + return []ersteller_lib.Route{googleLoginRoute, authenticatedRoute, logoutRoute} +} +func (a *Auth) saveEmailToSessionStore(c echo.Context, email string, userId int) error { + session, err := a.sessionStore.New(c.Request(), a.environment.SessionName) + if err != nil { + ersteller_lib.LogError("Failed to create session: %v", err) + return err + } + session.Values = map[interface{}]interface{}{ + a.environment.EmailSessionKey: email, + a.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 +} + +func (a *Auth) generateStateOauthCookie(w http.ResponseWriter) string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + ersteller_lib.LogError("Failed to read random state: %v", err) + } + state := base64.URLEncoding.EncodeToString(b) + + var expiration = time.Now().Add(time.Hour) + cookie := http.Cookie{Name: oAuthStateCookieName, Value: state, Expires: expiration, HttpOnly: true, Path: "/", Secure: false} + if a.isLocal { + cookie.SameSite = http.SameSiteLaxMode + cookie.Secure = false + } + http.SetCookie(w, &cookie) + + return state +} + +type GoogleUserData struct { + Id string `json:"id"` + Email string `json:"email"` + VerifiedEmail bool `json:"verified_email"` + Picture string `json:"picture"` + Hd string `json:"hd"` + Token *oauth2.Token +} diff --git a/authentication/google/google_auth.go b/authentication/google/google_auth.go new file mode 100644 index 0000000..be76318 --- /dev/null +++ b/authentication/google/google_auth.go @@ -0,0 +1,36 @@ +package google + +// AUTO GENERATED +// DO NOT EDIT + +import ( + "fmt" + "golang.org/x/oauth2" + "time" +) + +type Credentials struct { + Token oauth2.Token `json:"token"` +} + +type GoogleAuth struct { + Id int `db:"id"` + Credentials Credentials `db:"credentials"` + UserId int `db:"user_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (s GoogleAuth) String() string { + return fmt.Sprint("GoogleAuth{ ", + "Id: ", s.Id, ", ", + "Credentials: ", s.Credentials, ", ", + "UserId: ", s.UserId, ", ", + "CreatedAt: ", s.CreatedAt, ", ", + "UpdatedAt: ", s.UpdatedAt, ", ", + "}") +} + +func (s GoogleAuth) GetId() int { + return s.Id +} diff --git a/authentication/google/google_auth_repository.go b/authentication/google/google_auth_repository.go new file mode 100644 index 0000000..b3b44fa --- /dev/null +++ b/authentication/google/google_auth_repository.go @@ -0,0 +1,30 @@ +package google + +import ( + "context" + "errors" + "ersteller-lib" + "github.com/doug-martin/goqu/v9" +) + +func (r *GoogleAuthRepository) ReadByUserId(userId int) (GoogleAuth, error) { + ersteller_lib.Debug("Getting GoogleAuth by userId", userId) + sql, args, _ := r.dialect.From("googleAuth"). + Prepared(true). + Select(r.getSelectColumns()...). + Where(goqu.Ex{ + "user_id": userId, + }). + ToSQL() + + rows, err := r.connPool.Query(context.Background(), sql, args...) + if err != nil { + ersteller_lib.Error("Failed to get GoogleAuth: ", err) + } + defer rows.Close() + if rows.Next() { + item, _, err := r.rowToItem(rows, false) + return item, err + } + return GoogleAuth{}, errors.New("no rows found") +} diff --git a/authentication/google/googleauth-repository_gen.go b/authentication/google/googleauth-repository_gen.go new file mode 100644 index 0000000..90db51a --- /dev/null +++ b/authentication/google/googleauth-repository_gen.go @@ -0,0 +1,297 @@ +package google + +// GENERATED FILE +// DO NOT EDIT + +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 GoogleAuthRepository struct { + connPool *pgxpool.Pool + dialect goqu.DialectWrapper +} + +func NewGoogleAuthRepository(connPool *pgxpool.Pool) *GoogleAuthRepository { + return &GoogleAuthRepository{ + connPool: connPool, + dialect: goqu.Dialect("postgres"), + } +} + +func (r *GoogleAuthRepository) Create(googleAuth GoogleAuth) (int, error) { + sql, args, err := r.dialect.Insert("googleAuth"). + Prepared(true). + Rows(goqu.Record{ + + "updated_at": time.Now(), + "credentials": r.jsonToString(googleAuth.Credentials), + "user_id": googleAuth.UserId, + }). + Returning("id"). + ToSQL() + if err != nil { + ersteller_lib.LogError("error creating create GoogleAuth sql: %v", err) + return -1, err + } + + rows, err := r.connPool.Query(context.Background(), sql, args...) + if err != nil { + ersteller_lib.LogError("error creating GoogleAuth: %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("GoogleAuth already exists") + return -1, GoogleAuthAlreadyExistsError{GoogleAuth: googleAuth} + } + + return id, nil + +} + +type GoogleAuthAlreadyExistsError struct { + GoogleAuth GoogleAuth +} + +func (e GoogleAuthAlreadyExistsError) Error() string { + return fmt.Sprint("GoogleAuth ", e.GoogleAuth, " already exists") +} + +func (r *GoogleAuthRepository) getSelectColumns() []any { + return []any{"id", "created_at", "updated_at", + "credentials", "user_id", + } +} + +func (r *GoogleAuthRepository) Read(userId int, id int) (GoogleAuth, error) { + ersteller_lib.Debug("Getting GoogleAuth by id ", id) + sql, args, _ := r.dialect.From("googleAuth"). + Prepared(true). + Select(r.getSelectColumns()...). + Where(goqu.Ex{ + "id": id, + "user_id": userId, + }). + ToSQL() + + rows, err := r.connPool.Query(context.Background(), sql, args...) + if err != nil { + ersteller_lib.Error("Failed to get GoogleAuth: ", err) + } + defer rows.Close() + if rows.Next() { + item, _, err := r.rowToItem(rows, false) + return item, err + } + return GoogleAuth{}, errors.New("no rows found") +} + +type GoogleAuthItemScan struct { + GoogleAuth + RowId int + Count int +} + +func (r *GoogleAuthRepository) rowToItem(rows pgx.Rows, rowId bool) (GoogleAuth, int, error) { + var item GoogleAuthItemScan + if rowId { + err := rows.Scan( + &item.RowId, + &item.Count, + &item.Id, + &item.CreatedAt, + &item.UpdatedAt, + &item.Credentials, + &item.UserId, + ) + if err != nil { + return GoogleAuth{}, -1, err + } + } else { + err := rows.Scan( + &item.Id, + &item.CreatedAt, + &item.UpdatedAt, + &item.Credentials, + &item.UserId, + ) + if err != nil { + return GoogleAuth{}, -1, err + } + } + return GoogleAuth{ + Id: item.Id, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + Credentials: item.Credentials, + UserId: item.UserId, + }, item.Count, nil +} + +func (r *GoogleAuthRepository) Update(userId int, googleAuth GoogleAuth) error { + sql, args, err := r.dialect.Update("googleAuth"). + Prepared(true). + Set(goqu.Record{ + + "updated_at": time.Now(), + "credentials": r.jsonToString(googleAuth.Credentials), + "user_id": googleAuth.UserId, + }). + Where(goqu.Ex{ + "id": googleAuth.Id, + "user_id": userId, + }). + ToSQL() + if err != nil { + ersteller_lib.LogError("error creating update GoogleAuth sql: %v", err) + return err + } + + _, err = r.connPool.Exec(context.Background(), sql, args...) + if err != nil { + ersteller_lib.LogError("error updating GoogleAuth: %v", err) + return err + } + + return nil +} + +func (r *GoogleAuthRepository) Delete(userId int, id int) error { + sql, args, err := r.dialect.Delete("googleAuth"). + Prepared(true). + Where(goqu.Ex{ + "id": id, + "user_id": userId, + }). + ToSQL() + if err != nil { + ersteller_lib.LogError("error creating delete GoogleAuth sql: %v", err) + return err + } + + _, err = r.connPool.Exec(context.Background(), sql, args...) + if err != nil { + ersteller_lib.LogError("error deleting GoogleAuth: %v", err) + return err + } + + return nil +} + +type GoogleAuthField string + +const ( + GoogleAuthFieldCredentials GoogleAuthField = "credentials" +) + +type GoogleAuthOrderDirection string + +const ( + GoogleAuthOrderDirectionAsc GoogleAuthOrderDirection = "asc" + GoogleAuthOrderDirectionDesc GoogleAuthOrderDirection = "desc" +) + +type GoogleAuthReferences struct { + UserId int +} + +type GoogleAuthPaginationParams struct { + RowId int + PageSize int + OrderBy GoogleAuthField + OrderDirection GoogleAuthOrderDirection + + References GoogleAuthReferences +} + +func (r *GoogleAuthRepository) GetPage(params GoogleAuthPaginationParams) ([]GoogleAuth, int, error) { + var orderByWindow exp.WindowExpression + if params.OrderDirection == GoogleAuthOrderDirectionAsc { + 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{ + + "user_id": params.References.UserId, + }, + } + whereExpressions = r.addPageFilters(params, whereExpressions) + var colOrder exp.OrderedExpression + if params.OrderDirection == GoogleAuthOrderDirectionAsc { + colOrder = goqu.C(string(params.OrderBy)).Asc() + } else { + colOrder = goqu.C(string(params.OrderBy)).Desc() + } + dialect := goqu.Dialect("postgres") + innerFrom := dialect.From("googleAuth"). + 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([]GoogleAuth, 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 *GoogleAuthRepository) addPageFilters(params GoogleAuthPaginationParams, whereExpressions []goqu.Expression) []goqu.Expression { + + return whereExpressions +} + +func (r *GoogleAuthRepository) jsonToString(jsonData any) string { + bytes, err := json.Marshal(jsonData) + if err != nil { + return "{}" + } + return string(bytes) +} diff --git a/authentication/login_page.go b/authentication/login_page.go new file mode 100644 index 0000000..8930a9f --- /dev/null +++ b/authentication/login_page.go @@ -0,0 +1,59 @@ +package authentication + +import ( + "github.com/labstack/echo/v4" + "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" + "salezenify/google" + "salezenify/html_components/components" + "salezenify/html_components/route" + "salezenify/layout" + "salezenify/user" +) + +const LoginPath = "/login" + +type LoginPage struct { + createPage layout.CreatePageFunc + LoginRoute route.GetRoute +} + +func NewLoginPage(e *echo.Echo, createPage layout.CreatePageFunc) *LoginPage { + page := &LoginPage{createPage: createPage} + page.LoginRoute = route.NewGetRoute(LoginPath, page.Render).Add(e) + return page +} + +func (l *LoginPage) Render(c echo.Context) error { + return l.createPage(c, layout.PageWebsiteMetaData{ + WebsiteMetaData: components.WebsiteMetaData{ + Title: "Login", + }, + HideNavigation: true, + }, l.getLoginPage()) +} + +func (l *LoginPage) getLoginPage() gomponents.Group { + return []gomponents.Node{ + P( + A( + Href(user.OpenIdConnectPath), + Button( + Class(components.ButtonClass()), + Type("button"), + gomponents.Text("Login"), + ), + ), + ), + P( + A( + Href(google.GoogleLogin), + Button( + Class(components.ButtonClass()), + Type("button"), + gomponents.Text("Google Login"), + ), + ), + ), + } +} diff --git a/go.mod b/go.mod index f495fed..169e6f3 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,30 @@ module ersteller-lib go 1.24 require ( + github.com/doug-martin/goqu/v9 v9.19.0 + github.com/gorilla/sessions v1.4.0 github.com/jackc/pgx/v5 v5.7.5 + github.com/labstack/echo/v4 v4.13.4 github.com/mattn/go-sqlite3 v1.14.29 golang.org/x/crypto v0.40.0 + golang.org/x/oauth2 v0.30.0 + maragu.dev/gomponents v1.1.0 + maragu.dev/gomponents-htmx v0.6.1 ) require ( + cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..99b924a --- /dev/null +++ b/go.sum @@ -0,0 +1,79 @@ +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/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/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= +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/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +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/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +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/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo= +github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ= +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/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/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U= +maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM= +maragu.dev/gomponents-htmx v0.6.1 h1:vXXOkvqEDKYxSwD1UwqmVp12YwFSuM6u8lsRn7Evyng= +maragu.dev/gomponents-htmx v0.6.1/go.mod h1:51nXX+dTGff3usM7AJvbeOcQjzjpSycod+60CYeEP/M= diff --git a/page.go b/page.go new file mode 100644 index 0000000..b267ff6 --- /dev/null +++ b/page.go @@ -0,0 +1,40 @@ +package ersteller_lib + +import ( + "github.com/labstack/echo/v4" + . "maragu.dev/gomponents" +) + +type Language string + +const ( + En Language = "en" +) + +type NavItem struct { + Label string + Url string +} + +type WebsiteMetaData struct { + AppTitle string + Title string + Lang Language + Description string + NavItems []NavItem +} + +type PageWebsiteMetaData struct { + AppTitle string + Title string + Lang Language + Description string + NavItems []NavItem + ScriptSrcs []string + StyleSrcs []string + ActiveNavPath string + HideNavigation bool + UserEmail string +} + +type CreatePageFunc func(c echo.Context, metadata PageWebsiteMetaData, content ...Node) error diff --git a/route.go b/route.go new file mode 100644 index 0000000..abf06e4 --- /dev/null +++ b/route.go @@ -0,0 +1,144 @@ +package ersteller_lib + +import ( + "strings" + + "maragu.dev/gomponents" + hx "maragu.dev/gomponents-htmx" + + "github.com/labstack/echo/v4" +) + +type Route interface { + WithParams(params map[string]string) Route + ToUrl(params ...string) string + GetHtmx(params ...string) gomponents.Node +} + +//type RouteAdder interface { +// Route +//} + +type Router interface { + GET(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +type CommonRoute struct { + Path string + Params map[string]string + Handler echo.HandlerFunc +} + +func (r CommonRoute) Add(router Router) { + router.GET(r.Path, r.Handler) +} + +func (r CommonRoute) GetHtmx(params ...string) gomponents.Node { + return hx.Get(r.ToUrl(params...)) +} + +func NewCommonRoute(path string, handler echo.HandlerFunc) CommonRoute { + return CommonRoute{Path: path, Handler: handler, Params: make(map[string]string)} +} + +func (r CommonRoute) WithParams(params map[string]string) Route { + r.Params = params + return r +} + +func (r CommonRoute) ToUrl(params ...string) string { + for k, v := range r.Params { + r.Path = strings.ReplaceAll(r.Path, ":"+k, v) + } + if len(params) > 0 { + r.Path += "?" + strings.Join(params, "&") + } + return r.Path +} + +type GetRoute struct { + CommonRoute +} + +func NewGetRoute(path string, handler echo.HandlerFunc) GetRoute { + return GetRoute{CommonRoute: NewCommonRoute(path, handler)} +} + +func (r GetRoute) Add(router Router) GetRoute { + router.GET(r.Path, r.Handler) + return r +} + +func (r GetRoute) GetHtmx(params ...string) gomponents.Node { + return hx.Get(r.ToUrl(params...)) +} + +type PutRoute struct { + CommonRoute +} + +func NewPutRoute(path string, handler echo.HandlerFunc) PutRoute { + return PutRoute{CommonRoute: NewCommonRoute(path, handler)} +} + +func (r PutRoute) Add(router Router) PutRoute { + router.PUT(r.Path, r.Handler) + return r +} + +func (r PutRoute) WithParams(params map[string]string) Route { + r.Params = params + return r +} + +func (r PutRoute) GetHtmx(params ...string) gomponents.Node { + return hx.Put(r.ToUrl(params...)) +} + +type PostRoute struct { + CommonRoute +} + +func NewPostRoute(path string, handler echo.HandlerFunc) PostRoute { + return PostRoute{CommonRoute: NewCommonRoute(path, handler)} +} + +func (r PostRoute) Add(router Router) PostRoute { + router.POST(r.Path, r.Handler) + return r +} + +func (r PostRoute) WithParams(params map[string]string) Route { + r.Params = params + return r +} + +func (r PostRoute) GetHtmx(params ...string) gomponents.Node { + return hx.Post(r.ToUrl(params...)) +} + +type DeleteRoute struct { + CommonRoute +} + +func NewDeleteRoute(path string, handler echo.HandlerFunc) DeleteRoute { + return DeleteRoute{CommonRoute: NewCommonRoute(path, handler)} +} + +func (r DeleteRoute) Add(router Router) DeleteRoute { + router.DELETE(r.Path, r.Handler) + return r +} + +func (r DeleteRoute) WithParams(params map[string]string) Route { + r.Params = params + return r +} + +func (r DeleteRoute) GetHtmx(params ...string) gomponents.Node { + return hx.Delete(r.ToUrl(params...)) +}