feature/data persistence (#1180)

- **feat: add documentation and GitHub Actions workflow for publishing
documentation**
- **docs(concepts): add documentation for chain modules**
- **refactor: Simplify session management with SQLite storage and remove
deprecated code**
- **refactor: Simplify database initialization and remove
DatabaseContext**
- **refactor: move connection handling logic to resolver package**
- **feat: implement session management with database persistence**
- **feat: Ensure config directory exists when creating database path**
- **feat: Add SetUserHandle function to set user handle in session**
- **feat: Add public methods to set session fields with database save**
- **refactor: Remove unused session setter functions**
- **feat: Add getter methods for all Session Model properties**
- **feat: enhance Session model with user name details**
- **feat: add Motr support and update UI elements**
- **<no value>**
- **feat: Add unique handle constraint and method to check handle
existence**
- **docs: update site URL to onsonr.dev**
- **fix: correct import statement for database package**
- **test: updated CI to run tests on pull requests and merge groups**
- **docs: remove reference to develop branch in workflow**
- **feat: add WebAuthn support for user registration**
- **fix: correct smart account attenuation preset name**
- **feat: add ComputeIssuerDID and ComputeSonrAddr functions to ucan
package**
- **test: add unit tests for MPC keyset and keyshare**
- **feat: introduce new script to streamline GitHub issue creation**
This commit is contained in:
Prad Nukala
2024-12-06 21:31:20 -05:00
committed by GitHub
parent 94fb4dceac
commit 38447af730
47 changed files with 1992 additions and 725 deletions
+18 -12
View File
@@ -25,24 +25,30 @@ func HandleIndex(c echo.Context) error {
// Initial users have no authorization, user handle, or vault address
func isInitial(c echo.Context) bool {
noAuth := !session.HasAuthorization(c)
noUserHandle := !session.HasUserHandle(c)
noVaultAddress := !session.HasVaultAddress(c)
return noUserHandle && noVaultAddress && noAuth
sess, err := session.Get(c)
if err != nil {
return false
}
data := sess.Session()
return data.UserHandle == "" && data.VaultAddress == ""
}
// Expired users have either a user handle or vault address
func isExpired(c echo.Context) bool {
noAuth := !session.HasAuthorization(c)
hasUserHandle := session.HasUserHandle(c)
hasVaultAddress := session.HasVaultAddress(c)
return noAuth && hasUserHandle || noAuth && hasVaultAddress
sess, err := session.Get(c)
if err != nil {
return false
}
data := sess.Session()
return data.UserHandle != "" || data.VaultAddress != ""
}
// Returning users have a valid authorization, and either a user handle or vault address
func isReturning(c echo.Context) bool {
hasAuth := session.HasAuthorization(c)
hasUserHandle := session.HasUserHandle(c)
hasVaultAddress := session.HasVaultAddress(c)
return hasAuth && (hasUserHandle || hasVaultAddress)
sess, err := session.Get(c)
if err != nil {
return false
}
data := sess.Session()
return data.UserHandle != "" && data.VaultAddress != ""
}
+6 -15
View File
@@ -3,7 +3,6 @@ package handlers
import (
"net/http"
"github.com/cosmos/btcutil/bech32"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
"github.com/labstack/echo/v4"
@@ -22,23 +21,14 @@ func HandleRegisterView(env config.Env) echo.HandlerFunc {
}
func HandleRegisterStart(c echo.Context) error {
firstName := c.FormValue("first_name")
lastName := c.FormValue("last_name")
handle := c.FormValue("handle")
if firstName == "" || lastName == "" || handle == "" {
return response.RedirectLanding(c)
}
ks, err := mpc.NewKeyset()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
adr, err := bech32.Encode("idx", ks.Val().GetPublicKey())
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
req := getLinkCredentialRequest(c, adr, handle, ks.UserJSON())
req := getLinkCredentialRequest(c, ks.Address(), handle, ks.UserJSON())
return response.TemplEcho(c, register.LinkCredentialView(req))
}
@@ -60,14 +50,15 @@ func getLinkCredentialRequest(c echo.Context, addr string, handle string, userKS
RegisterOptions: buildRegisterOptions(buildUserEntity(addr, handle), buildLargeBlob(userKSJSON), buildServiceEntity(c)),
}
}
data := cc.Session()
usr := buildUserEntity(addr, handle)
blob := buildLargeBlob(userKSJSON)
service := buildServiceEntity(c)
return register.LinkCredentialRequest{
Platform: cc.BrowserName(),
Handle: handle,
DeviceModel: cc.BrowserVersion(),
Platform: data.BrowserName,
Handle: data.UserHandle,
DeviceModel: data.BrowserVersion,
Address: addr,
RegisterOptions: buildRegisterOptions(usr, blob, service),
}
-16
View File
@@ -1,16 +0,0 @@
package database
import (
"net/http"
"github.com/labstack/echo/v4"
)
var (
ErrInvalidCredentials = echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
ErrInvalidSubject = echo.NewHTTPError(http.StatusBadRequest, "Invalid subject")
ErrInvalidUser = echo.NewHTTPError(http.StatusBadRequest, "Invalid user")
ErrUserAlreadyExists = echo.NewHTTPError(http.StatusConflict, "User already exists")
ErrUserNotFound = echo.NewHTTPError(http.StatusNotFound, "User not found")
)
@@ -1,57 +0,0 @@
package database
import (
"os"
"path/filepath"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/gateway/config"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type DatabaseContext struct {
echo.Context
db *gorm.DB
}
func Middleware(env config.Env) echo.MiddlewareFunc {
cc := initDB(env.GetSqliteFile())
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc.Context = c
return next(cc)
}
}
}
func (c *DatabaseContext) HasDB() bool {
return c.db != nil
}
func initDB(path string) *DatabaseContext {
cc := new(DatabaseContext)
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
if err != nil {
cc.db = nil
return cc
}
// Migrate the schema
db.AutoMigrate(&Session{})
db.AutoMigrate(&User{})
return &DatabaseContext{
db: db,
}
}
func formatDBPath(path string) string {
home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
home = "."
}
return filepath.Join(home, ".config", "hway", path)
}
+31 -9
View File
@@ -1,19 +1,41 @@
package database
import (
"net/http"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
var (
ErrInvalidCredentials = echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
ErrInvalidSubject = echo.NewHTTPError(http.StatusBadRequest, "Invalid subject")
ErrInvalidUser = echo.NewHTTPError(http.StatusBadRequest, "Invalid user")
ErrUserAlreadyExists = echo.NewHTTPError(http.StatusConflict, "User already exists")
ErrUserNotFound = echo.NewHTTPError(http.StatusNotFound, "User not found")
)
type User struct {
gorm.Model
Address string `json:"address"`
Handle string `json:"handle"`
FirstName string `json:"firstName"`
LastInitial string `json:"lastInitial"`
VaultCID string `json:"vaultCID"`
}
type Session struct {
ID string `json:"id"`
gorm.Model
ID string `json:"id" gorm:"primaryKey"`
BrowserName string `json:"browserName"`
BrowserVersion string `json:"browserVersion"`
UserArchitecture string `json:"userArchitecture"`
Platform string `json:"platform"`
PlatformVersion string `json:"platformVersion"`
DeviceModel string `json:"deviceModel"`
}
type User struct {
Address string `json:"address"`
Handle string `json:"handle"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
VaultCID string `json:"vaultCID"`
UserHandle string `json:"userHandle"`
FirstName string `json:"firstName"`
LastInitial string `json:"lastInitial"`
VaultAddress string `json:"vaultAddress"`
}
+43
View File
@@ -0,0 +1,43 @@
package database
import (
"os"
"path/filepath"
"github.com/onsonr/sonr/pkg/gateway/config"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// InitDB initializes and returns a configured database connection
func InitDB(env config.Env) (*gorm.DB, error) {
path := formatDBPath(env.GetSqliteFile())
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
if err != nil {
return nil, err
}
// Migrate the schema
db.AutoMigrate(&Session{})
db.AutoMigrate(&User{})
return db, nil
}
func formatDBPath(path string) string {
home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
home = "."
}
configDir := filepath.Join(home, ".config", "hway")
if err := os.MkdirAll(configDir, 0755); err != nil {
// If we can't create the directory, fall back to current directory
return path
}
return filepath.Join(configDir, path)
}
+1 -1
View File
@@ -10,7 +10,7 @@ templ InitialView() {
@layout.Container() {
@text.Header("Sonr.ID", "The decentralized identity layer for the web.")
<div class="pt-3 flex flex-col items-center justify-center h-full">
<sl-button hx-target="#container" hx-get="/register" type="button">
<sl-button hx-target="#container" hx-get="/register" hx-push-url="/register" type="button">
<sl-icon slot="prefix" library="sonr" name="sonr"></sl-icon>
Get Started
<sl-icon slot="suffix" library="sonr" name="arrow-right"></sl-icon>
@@ -62,7 +62,7 @@ func InitialView() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"pt-3 flex flex-col items-center justify-center h-full\"><sl-button hx-target=\"#container\" hx-get=\"/register\" type=\"button\"><sl-icon slot=\"prefix\" library=\"sonr\" name=\"sonr\"></sl-icon> Get Started <sl-icon slot=\"suffix\" library=\"sonr\" name=\"arrow-right\"></sl-icon></sl-button></div>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"pt-3 flex flex-col items-center justify-center h-full\"><sl-button hx-target=\"#container\" hx-get=\"/register\" hx-push-url=\"/register\" type=\"button\"><sl-icon slot=\"prefix\" library=\"sonr\" name=\"sonr\"></sl-icon> Get Started <sl-icon slot=\"suffix\" library=\"sonr\" name=\"arrow-right\"></sl-icon></sl-button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+87 -24
View File
@@ -2,26 +2,18 @@ package session
import (
"net/http"
"regexp"
"strings"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/common"
"github.com/onsonr/sonr/pkg/gateway/internal/database"
"github.com/segmentio/ksuid"
"gorm.io/gorm"
)
type contextKey string
// Context keys
const (
DataContextKey contextKey = "http_session_data"
)
type SessionCtx interface {
ID() string
BrowserName() string
BrowserVersion() string
}
// Get returns the session.Context from the echo context.
func Get(c echo.Context) (SessionCtx, error) {
// Get returns the HTTPContext from the echo context
func Get(c echo.Context) (*HTTPContext, error) {
ctx, ok := c.(*HTTPContext)
if !ok {
return nil, echo.NewHTTPError(http.StatusInternalServerError, "Session Context not found")
@@ -29,18 +21,89 @@ func Get(c echo.Context) (SessionCtx, error) {
return ctx, nil
}
// TODO: Returns fixed chain ID for testing.
func GetChainID(c echo.Context) string {
return "sonr-testnet-1"
// HTTPContext is the context for HTTP endpoints.
type HTTPContext struct {
echo.Context
db *gorm.DB
sess *database.Session
}
// SetVaultAddress sets the address of the vault
func SetVaultAddress(c echo.Context, address string) error {
return common.WriteCookie(c, common.SonrAddress, address)
// NewHTTPContext creates a new session context
func NewHTTPContext(c echo.Context, db *gorm.DB) *HTTPContext {
return &HTTPContext{
Context: c,
db: db,
}
}
// SetVaultAuthorization sets the UCAN CID of the vault
func SetVaultAuthorization(c echo.Context, ucanCID string) error {
common.HeaderWrite(c, common.Authorization, formatAuth(ucanCID))
// Session returns the current session
func (s *HTTPContext) Session() *database.Session {
return s.sess
}
// InitSession initializes or loads an existing session
func (s *HTTPContext) InitSession() error {
sessionID := s.getOrCreateSessionID()
// Try to load existing session
var sess database.Session
result := s.db.Where("id = ?", sessionID).First(&sess)
if result.Error != nil {
// Create new session if not found
bn, bv := extractBrowserInfo(s.Context)
sess = database.Session{
ID: sessionID,
BrowserName: bn,
BrowserVersion: bv,
}
if err := s.db.Create(&sess).Error; err != nil {
return err
}
}
s.sess = &sess
return nil
}
func (s *HTTPContext) getOrCreateSessionID() string {
if ok := common.CookieExists(s.Context, common.SessionID); !ok {
sessionID := ksuid.New().String()
common.WriteCookie(s.Context, common.SessionID, sessionID)
return sessionID
}
sessionID, err := common.ReadCookie(s.Context, common.SessionID)
if err != nil {
sessionID = ksuid.New().String()
common.WriteCookie(s.Context, common.SessionID, sessionID)
}
return sessionID
}
func extractBrowserInfo(c echo.Context) (string, string) {
userAgent := common.HeaderRead(c, common.UserAgent)
if userAgent == "" {
return "N/A", "-1"
}
var name, ver string
entries := strings.Split(strings.TrimSpace(userAgent), ",")
for _, entry := range entries {
entry = strings.TrimSpace(entry)
re := regexp.MustCompile(`"([^"]+)";v="([^"]+)"`)
matches := re.FindStringSubmatch(entry)
if len(matches) == 3 {
browserName := matches[1]
version := matches[2]
if browserName != common.BrowserNameUnknown.String() &&
browserName != common.BrowserNameChromium.String() {
name = browserName
ver = version
break
}
}
}
return name, ver
}
+7 -37
View File
@@ -2,48 +2,18 @@ package session
import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/common"
"github.com/onsonr/sonr/pkg/gateway/config"
"gorm.io/gorm"
)
// Middleware establishes a Session Cookie.
func Middleware(env config.Env) echo.MiddlewareFunc {
// Middleware creates a new session middleware
func Middleware(db *gorm.DB) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := injectSession(c, common.RoleHway)
cc := NewHTTPContext(c, db)
if err := cc.InitSession(); err != nil {
return err
}
return next(cc)
}
}
}
// injectSession returns the session injectSession from the cookies.
func injectSession(c echo.Context, role common.PeerRole) *HTTPContext {
if c == nil {
return initHTTPContext(nil)
}
common.WriteCookie(c, common.SessionRole, role.String())
// Continue even if there are errors, just ensure we have valid session data
if err := loadOrGenKsuid(c); err != nil {
// Log error but continue
}
return initHTTPContext(c)
}
// HasAuthorization checks if the request has an authorization header
func HasAuthorization(c echo.Context) bool {
return common.HeaderExists(c, common.Authorization)
}
// HasUserHandle checks if the request has a user handle cookie
func HasUserHandle(c echo.Context) bool {
return common.CookieExists(c, common.UserHandle)
}
// HasVaultAddress checks if the request has a vault address cookie
func HasVaultAddress(c echo.Context) bool {
return common.CookieExists(c, common.SonrAddress)
}
+154 -170
View File
@@ -1,184 +1,168 @@
package session
import (
"regexp"
"strings"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
"github.com/labstack/echo/v4"
"github.com/segmentio/ksuid"
"github.com/onsonr/sonr/pkg/common"
"github.com/onsonr/sonr/pkg/gateway/internal/database"
)
const kWebAuthnTimeout = 6000
// ╭───────────────────────────────────────────────────────╮
// │ DB Setter Functions │
// ╰───────────────────────────────────────────────────────╯
// HTTPContext is the context for HTTP endpoints.
type HTTPContext struct {
echo.Context
role common.PeerRole
id string
chal string
bn string
bv string
// SetUserHandle sets the user handle in the session
func SetUserHandle(c echo.Context, handle string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().UserHandle = handle
return sess.db.Save(sess.Session()).Error
}
// initHTTPContext loads the headers from the request.
func initHTTPContext(c echo.Context) *HTTPContext {
if c == nil {
return &HTTPContext{}
// SetFirstName sets the first name in the session
func SetFirstName(c echo.Context, name string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().FirstName = name
return sess.db.Save(sess.Session()).Error
}
// SetLastInitial sets the last initial in the session
func SetLastInitial(c echo.Context, initial string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().LastInitial = initial
return sess.db.Save(sess.Session()).Error
}
// SetVaultAddress sets the vault address in the session
func SetVaultAddress(c echo.Context, address string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().VaultAddress = address
return sess.db.Save(sess.Session()).Error
}
// ╭───────────────────────────────────────────────────────╮
// │ DB Getter Functions │
// ╰───────────────────────────────────────────────────────╯
// GetID returns the session ID
func GetID(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().ID, nil
}
// GetBrowserName returns the browser name
func GetBrowserName(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().BrowserName, nil
}
// GetBrowserVersion returns the browser version
func GetBrowserVersion(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().BrowserVersion, nil
}
// GetUserArchitecture returns the user architecture
func GetUserArchitecture(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().UserArchitecture, nil
}
// GetPlatform returns the platform
func GetPlatform(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().Platform, nil
}
// GetPlatformVersion returns the platform version
func GetPlatformVersion(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().PlatformVersion, nil
}
// GetDeviceModel returns the device model
func GetDeviceModel(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().DeviceModel, nil
}
// GetUserHandle returns the user handle
func GetUserHandle(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().UserHandle, nil
}
// GetFirstName returns the first name
func GetFirstName(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().FirstName, nil
}
// GetLastInitial returns the last initial
func GetLastInitial(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().LastInitial, nil
}
// GetVaultAddress returns the vault address
func GetVaultAddress(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().VaultAddress, nil
}
// HandleExists checks if a handle already exists in any session
func HandleExists(c echo.Context, handle string) (bool, error) {
sess, err := Get(c)
if err != nil {
return false, err
}
id, chal := extractPeerInfo(c)
bn, bv := extractBrowserInfo(c)
cc := &HTTPContext{
Context: c,
role: common.PeerRole(common.ReadCookieUnsafe(c, common.SessionRole)),
id: id,
chal: chal,
bn: bn,
bv: bv,
var count int64
if err := sess.db.Model(&database.Session{}).Where("user_handle = ?", handle).Count(&count).Error; err != nil {
return false, err
}
// Set the session data in both contexts
return cc
}
func (s *HTTPContext) ID() string {
return s.id
}
func (s *HTTPContext) BrowserName() string {
return s.bn
}
func (s *HTTPContext) BrowserVersion() string {
return s.bv
}
// ╭───────────────────────────────────────────────────────────╮
// │ Initialization │
// ╰───────────────────────────────────────────────────────────╯
func loadOrGenKsuid(c echo.Context) error {
var (
sessionID string
err error
)
// Setup genKsuid function
genKsuid := func() string {
return ksuid.New().String()
}
// Attempt to read the session ID from the "session" cookie
if ok := common.CookieExists(c, common.SessionID); !ok {
sessionID = genKsuid()
} else {
sessionID, err = common.ReadCookie(c, common.SessionID)
if err != nil {
sessionID = genKsuid()
}
}
common.WriteCookie(c, common.SessionID, sessionID)
return nil
}
// ╭───────────────────────────────────────────────────────────╮
// │ Extraction │
// ╰───────────────────────────────────────────────────────────╯
func extractPeerInfo(c echo.Context) (string, string) {
var chal protocol.URLEncodedBase64
id, _ := common.ReadCookie(c, common.SessionID)
chalRaw, _ := common.ReadCookieBytes(c, common.SessionChallenge)
chal.UnmarshalJSON(chalRaw)
return id, common.Base64Encode(chal)
}
func extractBrowserInfo(c echo.Context) (string, string) {
secCHUA := common.HeaderRead(c, common.UserAgent)
// If common.is empty, return empty BrowserInfo
if secCHUA == "" {
return "N/A", "-1"
}
// Split the common.into individual browser entries
var (
name string
ver string
)
entries := strings.Split(strings.TrimSpace(secCHUA), ",")
for _, entry := range entries {
// Remove leading/trailing spaces and quotes
entry = strings.TrimSpace(entry)
// Use regex to extract the browser name and version
re := regexp.MustCompile(`"([^"]+)";v="([^"]+)"`)
matches := re.FindStringSubmatch(entry)
if len(matches) == 3 {
browserName := matches[1]
version := matches[2]
// Skip "Not A;Brand"
if !validBrowser(browserName) {
continue
}
// Store the first valid browser info as fallback
name = browserName
ver = version
}
}
return name, ver
}
func validBrowser(name string) bool {
return name != common.BrowserNameUnknown.String() && name != common.BrowserNameChromium.String()
}
// ╭───────────────────────────────────────────────────────────╮
// │ Authentication │
// ╰───────────────────────────────────────────────────────────╯
func buildUserEntity(userID string) protocol.UserEntity {
return protocol.UserEntity{
ID: userID,
}
}
// returns the base options for registering a new user without challenge or user entity.
func baseRegisterOptions() *protocol.PublicKeyCredentialCreationOptions {
return &protocol.PublicKeyCredentialCreationOptions{
Timeout: kWebAuthnTimeout,
Attestation: protocol.PreferDirectAttestation,
AuthenticatorSelection: protocol.AuthenticatorSelection{
AuthenticatorAttachment: "platform",
ResidentKey: protocol.ResidentKeyRequirementPreferred,
UserVerification: "preferred",
},
Parameters: []protocol.CredentialParameter{
{
Type: "public-key",
Algorithm: webauthncose.AlgES256,
},
{
Type: "public-key",
Algorithm: webauthncose.AlgES256K,
},
{
Type: "public-key",
Algorithm: webauthncose.AlgEdDSA,
},
},
}
}
func formatAuth(ucanCID string) string {
return "Bearer " + ucanCID
return count > 0, nil
}
+11 -4
View File
@@ -10,16 +10,23 @@ import (
"github.com/onsonr/sonr/pkg/gateway/internal/session"
)
func RegisterRoutes(e *echo.Echo, env config.Env) {
func RegisterRoutes(e *echo.Echo, env config.Env) error {
// Custom error handler for gateway
e.HTTPErrorHandler = response.RedirectOnError("http://localhost:3000")
// Inject session middleware
e.Use(session.Middleware(env))
e.Use(database.Middleware(env))
// Initialize database
db, err := database.InitDB(env)
if err != nil {
return err
}
// Inject session middleware with database connection
e.Use(session.Middleware(db))
// Register routes
e.GET("/", handlers.HandleIndex)
e.GET("/register", handlers.HandleRegisterView(env))
e.POST("/register/start", handlers.HandleRegisterStart)
e.POST("/register/finish", handlers.HandleRegisterFinish)
return nil
}