diff --git a/domains/auth/find.go b/domains/auth/find.go index 419a3f9..72d07b3 100644 --- a/domains/auth/find.go +++ b/domains/auth/find.go @@ -96,6 +96,9 @@ func FetchWorkspaceByID(ctx context.Context, id int64) (*Workspace, error) { return WorkspaceFromDB(&dw), nil } +// FetchWorkspacesForUser fetches all workspaces for a user +// if no workspaces are found, it returns an empty slice +// if an error occurs with db query, it returns the error func FetchWorkspacesForUser(ctx context.Context, userID int64) ([]*Workspace, error) { dws, err := db.Q.GetWorkspacesForUser(ctx, userID) if err != nil && !errors.Is(err, pgx.ErrNoRows) { diff --git a/routers/auth/dom.go b/routers/auth/dom.go deleted file mode 100644 index ba2abc5..0000000 --- a/routers/auth/dom.go +++ /dev/null @@ -1,29 +0,0 @@ -package auth - -import ( - "github.com/karngyan/maek/domains/auth" - "github.com/karngyan/maek/routers/models" -) - -func modelForWorkspace(workspace *auth.Workspace) *models.Workspace { - return &models.Workspace{ - ID: workspace.ID, - Name: workspace.Name, - Description: workspace.Description, - Created: workspace.Created, - Updated: workspace.Updated, - } -} - -func modelForAuthBundle(bundle *auth.Bundle) map[string]any { - u := models.ModelForUser(bundle.User) - wss := make([]*models.Workspace, 0, len(bundle.Workspaces)) - for _, ws := range bundle.Workspaces { - wss = append(wss, modelForWorkspace(ws)) - } - - return map[string]any{ - "user": u, - "workspaces": wss, - } -} diff --git a/routers/auth/info.go b/routers/auth/info.go deleted file mode 100644 index 4cd2354..0000000 --- a/routers/auth/info.go +++ /dev/null @@ -1,16 +0,0 @@ -package auth - -import ( - "net/http" - - "github.com/karngyan/maek/domains/auth" - "github.com/karngyan/maek/routers/base" -) - -func Info(ctx *base.WebContext) { - base.Respond(ctx, modelForAuthBundle(&auth.Bundle{ - User: ctx.User, - Session: ctx.Session, - Workspaces: ctx.AllWorkspaces, - }), http.StatusOK) -} diff --git a/routers/auth/login.go b/routers/auth/login.go deleted file mode 100644 index bd7715a..0000000 --- a/routers/auth/login.go +++ /dev/null @@ -1,66 +0,0 @@ -package auth - -import ( - "errors" - "net/http" - "net/mail" - "strings" - - "github.com/karngyan/maek/domains/auth" - "github.com/karngyan/maek/routers/base" -) - -type mpa map[string]any - -func Login(ctx *base.WebContext) { - var req struct { - Email string `json:"email"` - Password string `json:"password"` - Remember bool `json:"remember"` - } - - if err := ctx.DecodeJSON(&req); err != nil { - base.UnprocessableEntity(ctx, err) - return - } - - req.Email = strings.TrimSpace(req.Email) - - _, err := mail.ParseAddress(req.Email) - if err != nil { - base.BadRequest(ctx, mpa{ - "email": "Invalid email address", - }) - return - } - - bundle, err := auth.Login(ctx.Request.Context(), req.Email, req.Password, req.Remember, ctx.Input.IP(), ctx.Input.UserAgent()) - if err != nil { - if errors.Is(err, auth.ErrUserNotFound) { - base.BadRequest(ctx, mpa{ - "email": "User not found with this email", - }) - return - } - - if errors.Is(err, auth.ErrInvalidPassword) { - base.BadRequest(ctx, mpa{ - "password": "Password is incorrect", - }) - return - } - - base.InternalError(ctx, err) - return - } - - base.RespondCookie(ctx, modelForAuthBundle(bundle), http.StatusOK, &http.Cookie{ - Name: "session_token", - Value: bundle.Session.Token, - Path: "/", - MaxAge: int(bundle.Session.Age().Seconds()), // less error prone than Expires - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - }) -} diff --git a/routers/auth/login_test.go b/routers/auth/login_test.go deleted file mode 100644 index 2e3f9ec..0000000 --- a/routers/auth/login_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package auth_test - -import ( - "testing" - - approvals "github.com/approvals/go-approval-tests" - - "github.com/karngyan/maek/zarf/tests" - "github.com/stretchr/testify/assert" -) - -func TestLogin(t *testing.T) { - defer tests.TruncateTables() - - cs := tests.NewClientStateWithUser(t) - rr, err := cs.Post("/v1/auth/login", map[string]any{ - "email": "karn@maek.ai", - "password": "test-password", - }) - assert.Nil(t, err) - assert.Equal(t, 200, rr.Code) - - approvals.VerifyJSONBytes(t, rr.Body.Bytes()) - assert.Contains(t, rr.Header().Get("Set-Cookie"), "HttpOnly; Secure; SameSite=Strict") -} - -func TestLoginErrors(t *testing.T) { - defer tests.TruncateTables() - cs := tests.NewClientStateWithUser(t) - - type testCase struct { - name string - body map[string]any - expectedCode int - } - - tcs := []testCase{ - { - name: "Invalid email", - body: map[string]any{ - "email": "wrong-email", - "password": "test-password", - }, - expectedCode: 400, - }, - { - name: "Invalid password", - body: map[string]any{ - "email": "karn@maek.ai", - "password": "wrong-password", - }, - expectedCode: 400, - }, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - rr, err := cs.Post("/v1/auth/login", tc.body) - assert.Nil(t, err) - assert.Equal(t, tc.expectedCode, rr.Code) - approvals.VerifyJSONBytes(t, rr.Body.Bytes()) - }) - } -} diff --git a/routers/auth/router.go b/routers/auth/router.go deleted file mode 100644 index b4caae7..0000000 --- a/routers/auth/router.go +++ /dev/null @@ -1,15 +0,0 @@ -package auth - -import ( - "github.com/beego/beego/v2/core/logs" - "github.com/beego/beego/v2/server/web" - - "github.com/karngyan/maek/routers/base" -) - -func Configure(l *logs.BeeLogger) { - web.Post("/v1/auth/register", base.WrapPublicRoute(Register, l)) - web.Post("/v1/auth/login", base.WrapPublicRoute(Login, l)) - web.Get("/v1/auth/logout", base.WrapAuthenticated(Logout, l)) - web.Get("/v1/auth/info", base.WrapAuthenticatedWithUserAllWorkspaces(Info, l)) -} diff --git a/routers/auth/setup_test.go b/routers/auth/setup_test.go deleted file mode 100644 index fbdbe43..0000000 --- a/routers/auth/setup_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package auth_test - -import ( - "os" - "testing" - - approvals "github.com/approvals/go-approval-tests" - - "github.com/karngyan/maek/zarf/tests" -) - -func TestMain(m *testing.M) { - tests.FreezeTime() - os.Exit(runTests(m)) -} - -func runTests(m *testing.M) int { - cleanup, err := tests.InitApp() - if err != nil { - cleanup() - return 1 - } - defer cleanup() - - approvals.UseFolder("./testdata") - - return m.Run() -} diff --git a/routers/auth/testdata/login_test.TestLogin.approved.json b/routers/auth/testdata/login_test.TestLogin.approved.json deleted file mode 100644 index e34264f..0000000 --- a/routers/auth/testdata/login_test.TestLogin.approved.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "user": { - "created": 1234567890, - "defaultWorkspaceId": 1, - "email": "karn@maek.ai", - "id": 1, - "name": "Karn", - "role": "admin", - "updated": 1234567890, - "verified": false - }, - "workspaces": [ - { - "created": 1234567890, - "description": "default workspace", - "id": 1, - "name": "default", - "updated": 1234567890 - } - ] -} \ No newline at end of file diff --git a/routers/auth/testdata/login_test.TestLoginErrors.Invalid_email.approved.json b/routers/auth/testdata/login_test.TestLoginErrors.Invalid_email.approved.json deleted file mode 100644 index de6ba02..0000000 --- a/routers/auth/testdata/login_test.TestLoginErrors.Invalid_email.approved.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "Invalid email address" -} \ No newline at end of file diff --git a/routers/auth/testdata/login_test.TestLoginErrors.Invalid_password.approved.json b/routers/auth/testdata/login_test.TestLoginErrors.Invalid_password.approved.json deleted file mode 100644 index 823012d..0000000 --- a/routers/auth/testdata/login_test.TestLoginErrors.Invalid_password.approved.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "password": "Password is incorrect" -} \ No newline at end of file diff --git a/routers/auth/testdata/register_test.TestRegister.approved.json b/routers/auth/testdata/register_test.TestRegister.approved.json deleted file mode 100644 index e34264f..0000000 --- a/routers/auth/testdata/register_test.TestRegister.approved.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "user": { - "created": 1234567890, - "defaultWorkspaceId": 1, - "email": "karn@maek.ai", - "id": 1, - "name": "Karn", - "role": "admin", - "updated": 1234567890, - "verified": false - }, - "workspaces": [ - { - "created": 1234567890, - "description": "default workspace", - "id": 1, - "name": "default", - "updated": 1234567890 - } - ] -} \ No newline at end of file diff --git a/routers/auth/testdata/register_test.TestRegisterErrors.Invalid_email.approved.json b/routers/auth/testdata/register_test.TestRegisterErrors.Invalid_email.approved.json deleted file mode 100644 index de6ba02..0000000 --- a/routers/auth/testdata/register_test.TestRegisterErrors.Invalid_email.approved.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "Invalid email address" -} \ No newline at end of file diff --git a/routers/auth/testdata/register_test.TestRegisterErrors.too_long_name.approved.json b/routers/auth/testdata/register_test.TestRegisterErrors.too_long_name.approved.json deleted file mode 100644 index 0bce91d..0000000 --- a/routers/auth/testdata/register_test.TestRegisterErrors.too_long_name.approved.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Must be at most 200 characters long" -} \ No newline at end of file diff --git a/routers/auth/testdata/register_test.TestRegisterErrors.too_long_password.approved.json b/routers/auth/testdata/register_test.TestRegisterErrors.too_long_password.approved.json deleted file mode 100644 index d577832..0000000 --- a/routers/auth/testdata/register_test.TestRegisterErrors.too_long_password.approved.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "password": "Must be at most 64 characters long" -} \ No newline at end of file diff --git a/routers/auth/testdata/register_test.TestRegisterErrors.too_short_password.approved.json b/routers/auth/testdata/register_test.TestRegisterErrors.too_short_password.approved.json deleted file mode 100644 index 99855ca..0000000 --- a/routers/auth/testdata/register_test.TestRegisterErrors.too_short_password.approved.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "password": "Must be at least 6 characters long" -} \ No newline at end of file diff --git a/routers/routers.go b/routers/routers.go index ab350d3..b50e8d5 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -2,13 +2,11 @@ package routers import ( "github.com/beego/beego/v2/core/logs" - "github.com/karngyan/maek/routers/auth" "github.com/karngyan/maek/routers/collections" "github.com/karngyan/maek/routers/notes" ) func Init(l *logs.BeeLogger) error { - auth.Configure(l) notes.Configure(l) collections.Configure(l) return nil diff --git a/ui_api/auth/info.go b/ui_api/auth/info.go new file mode 100644 index 0000000..0f08ac4 --- /dev/null +++ b/ui_api/auth/info.go @@ -0,0 +1,17 @@ +package auth + +import ( + "net/http" + + "github.com/karngyan/maek/domains/auth" + "github.com/karngyan/maek/ui_api/models" + "github.com/karngyan/maek/ui_api/web" +) + +func info(ctx web.Context) error { + return ctx.JSON(http.StatusOK, models.ModelForAuthBundle(&auth.Bundle{ + User: ctx.User, + Session: ctx.Session, + Workspaces: ctx.AllWorkspaces, + })) +} diff --git a/ui_api/auth/login_test.go b/ui_api/auth/login_test.go index db60944..811d807 100644 --- a/ui_api/auth/login_test.go +++ b/ui_api/auth/login_test.go @@ -23,3 +23,42 @@ func TestLogin(t *testing.T) { approvals.VerifyJSONBytes(t, rr.Body.Bytes()) assert.Contains(t, rr.Header().Get("Set-Cookie"), "HttpOnly; Secure; SameSite=Strict") } + +func TestLoginErrors(t *testing.T) { + defer testutil.TruncateTables() + cs := testutil.NewClientStateWithUser(t) + + type testCase struct { + name string + body map[string]any + expectedCode int + } + + tcs := []testCase{ + { + name: "Invalid email", + body: map[string]any{ + "email": "wrong-email", + "password": "test-password", + }, + expectedCode: 400, + }, + { + name: "Invalid password", + body: map[string]any{ + "email": "karn@maek.ai", + "password": "wrong-password", + }, + expectedCode: 400, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + rr, err := cs.Post("/v1/auth/login", tc.body) + assert.Nil(t, err) + assert.Equal(t, tc.expectedCode, rr.Code) + approvals.VerifyJSONBytes(t, rr.Body.Bytes()) + }) + } +} diff --git a/routers/auth/logout.go b/ui_api/auth/logout.go similarity index 60% rename from routers/auth/logout.go rename to ui_api/auth/logout.go index 5b27f22..0720666 100644 --- a/routers/auth/logout.go +++ b/ui_api/auth/logout.go @@ -4,20 +4,18 @@ import ( "net/http" "github.com/karngyan/maek/domains/auth" - - "github.com/karngyan/maek/routers/base" + "github.com/karngyan/maek/ui_api/web" ) -func Logout(ctx *base.WebContext) { - rctx := ctx.Request.Context() +func logout(ctx web.Context) error { + rctx := ctx.Request().Context() err := auth.DeleteSession(rctx, ctx.Session.Token) if err != nil { - base.InternalError(ctx, err) - return + return ctx.InternalError(err) } - base.RespondCookie(ctx, nil, http.StatusOK, &http.Cookie{ + ctx.SetCookie(&http.Cookie{ Name: "session_token", Value: "", Path: "/", @@ -26,4 +24,6 @@ func Logout(ctx *base.WebContext) { Secure: true, SameSite: http.SameSiteStrictMode, }) + + return ctx.NoContent(http.StatusOK) } diff --git a/routers/auth/logout_test.go b/ui_api/auth/logout_test.go similarity index 78% rename from routers/auth/logout_test.go rename to ui_api/auth/logout_test.go index 263fdba..ae03ada 100644 --- a/routers/auth/logout_test.go +++ b/ui_api/auth/logout_test.go @@ -5,17 +5,16 @@ import ( "errors" "testing" - "github.com/karngyan/maek/domains/auth" - "github.com/stretchr/testify/assert" - "github.com/karngyan/maek/zarf/tests" + "github.com/karngyan/maek/domains/auth" + "github.com/karngyan/maek/ui_api/testutil" ) func TestLogout(t *testing.T) { - defer tests.TruncateTables() + defer testutil.TruncateTables() - cs := tests.NewClientStateWithUser(t) + cs := testutil.NewClientStateWithUser(t) sessionToken := cs.Session.Token rr, err := cs.Get("/v1/auth/logout") diff --git a/routers/auth/register.go b/ui_api/auth/register.go similarity index 64% rename from routers/auth/register.go rename to ui_api/auth/register.go index 7234265..098582e 100644 --- a/routers/auth/register.go +++ b/ui_api/auth/register.go @@ -7,7 +7,8 @@ import ( "strings" "github.com/karngyan/maek/domains/auth" - "github.com/karngyan/maek/routers/base" + "github.com/karngyan/maek/ui_api/models" + "github.com/karngyan/maek/ui_api/web" ) const ( @@ -16,16 +17,17 @@ const ( maxNameLength = 200 ) -func Register(ctx *base.WebContext) { +func register(ctx web.Context) error { var req struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` } - if err := ctx.DecodeJSON(&req); err != nil { - base.UnprocessableEntity(ctx, err) - return + if err := ctx.Bind(&req); err != nil { + return ctx.JSON(http.StatusUnprocessableEntity, mpa{ + "error": err.Error(), + }) } req.Email = strings.TrimSpace(req.Email) @@ -34,50 +36,44 @@ func Register(ctx *base.WebContext) { _, err := mail.ParseAddress(req.Email) if err != nil { - base.BadRequest(ctx, mpa{ + return ctx.JSON(http.StatusBadRequest, mpa{ "email": "Invalid email address", }) - return } if len(req.Password) < minPasswordLength { - base.BadRequest(ctx, mpa{ + return ctx.JSON(http.StatusBadRequest, mpa{ "password": "Must be at least 6 characters long", }) - return } if len(req.Password) > maxPasswordLength { - base.BadRequest(ctx, mpa{ + return ctx.JSON(http.StatusBadRequest, mpa{ "password": "Must be at most 64 characters long", }) - return } if len(req.Name) > maxNameLength { - base.BadRequest(ctx, mpa{ + return ctx.JSON(http.StatusBadRequest, mpa{ "name": "Must be at most 200 characters long", }) - return } - rctx := ctx.Request.Context() + rctx := ctx.Request().Context() - bundle, err := auth.CreateDefaultWorkspaceWithUser(rctx, req.Name, req.Email, req.Password, ctx.Input.IP(), ctx.Input.UserAgent()) + bundle, err := auth.CreateDefaultWorkspaceWithUser(rctx, req.Name, req.Email, req.Password, ctx.RealIP(), ctx.Request().Header.Get("User-Agent")) if err != nil { if errors.Is(err, auth.ErrUserAlreadyExists) { - base.BadRequest(ctx, mpa{ + return ctx.JSON(http.StatusBadRequest, mpa{ "email": "User already exists with this email", }) - return } - base.InternalError(ctx, err) - return + return ctx.InternalError(err) } - base.RespondCookie(ctx, modelForAuthBundle(bundle), http.StatusCreated, &http.Cookie{ + ctx.SetCookie(&http.Cookie{ Name: "session_token", Value: bundle.Session.Token, Path: "/", @@ -86,4 +82,6 @@ func Register(ctx *base.WebContext) { HttpOnly: true, SameSite: http.SameSiteStrictMode, }) + + return ctx.JSON(http.StatusCreated, models.ModelForAuthBundle(bundle)) } diff --git a/routers/auth/register_test.go b/ui_api/auth/register_test.go similarity index 91% rename from routers/auth/register_test.go rename to ui_api/auth/register_test.go index 1e3a453..a43814d 100644 --- a/routers/auth/register_test.go +++ b/ui_api/auth/register_test.go @@ -4,14 +4,15 @@ import ( "testing" approvals "github.com/approvals/go-approval-tests" + "github.com/karngyan/maek/libs/randstr" - "github.com/karngyan/maek/zarf/tests" + "github.com/karngyan/maek/ui_api/testutil" "github.com/stretchr/testify/assert" ) func TestRegister(t *testing.T) { - defer tests.TruncateTables() - cs := tests.NewClientState() + defer testutil.TruncateTables() + cs := testutil.NewClientState() rr, err := cs.Post("/v1/auth/register", map[string]any{ "name": "Karn", @@ -27,7 +28,7 @@ func TestRegister(t *testing.T) { } func TestRegisterErrors(t *testing.T) { - cs := tests.NewClientState() + cs := testutil.NewClientState() type testCase struct { name string diff --git a/ui_api/auth/router.go b/ui_api/auth/router.go index 82b2f10..c9a614c 100644 --- a/ui_api/auth/router.go +++ b/ui_api/auth/router.go @@ -7,5 +7,8 @@ import ( ) func Configure(e *echo.Echo, l *zap.Logger) { + e.POST("/v1/auth/register", web.WrapPublicRoute(register, l)) e.POST("/v1/auth/login", web.WrapPublicRoute(login, l)) + e.GET("/v1/auth/logout", web.WrapAuthenticated(logout, l)) + e.GET("/v1/auth/info", web.WrapAuthenticatedWithUserAllWorkspaces(info, l)) } diff --git a/ui_api/models/auth.go b/ui_api/models/auth.go index 8ecef1a..9dffec2 100644 --- a/ui_api/models/auth.go +++ b/ui_api/models/auth.go @@ -28,6 +28,14 @@ func ModelForUser(user *auth.User) *User { } } +type Workspace struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` +} + func ModelForWorkspace(workspace *auth.Workspace) *Workspace { return &Workspace{ ID: workspace.ID, diff --git a/ui_api/models/workspace.go b/ui_api/models/workspace.go deleted file mode 100644 index 78f8605..0000000 --- a/ui_api/models/workspace.go +++ /dev/null @@ -1,9 +0,0 @@ -package models - -type Workspace struct { - ID int64 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Created int64 `json:"created"` - Updated int64 `json:"updated"` -} diff --git a/ui_api/web/context.go b/ui_api/web/context.go index a6a0433..120bb3a 100644 --- a/ui_api/web/context.go +++ b/ui_api/web/context.go @@ -1,9 +1,12 @@ package web import ( + "errors" "fmt" "net/http" + "github.com/bluele/go-timecop" + "github.com/karngyan/maek/libs/randstr" "github.com/labstack/echo/v4" @@ -35,6 +38,22 @@ func (c Context) InternalError(err error) error { return c.JSON(http.StatusInternalServerError, resp) } +func (c Context) Unauthorized() error { + c.SetCookie(&http.Cookie{ + Name: "session_token", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + }) + + return c.JSON(http.StatusUnauthorized, map[string]any{ + "error": "Unauthorized", + }) +} + type HandlerFunc func(ctx Context) error func WrapPublicRoute(h HandlerFunc, l *zap.Logger) echo.HandlerFunc { @@ -53,3 +72,106 @@ func public(h HandlerFunc, l *zap.Logger) echo.HandlerFunc { return h(ctx) } } + +func WrapAuthenticated(h HandlerFunc, l *zap.Logger) echo.HandlerFunc { + return authenticated(h, l, false, false, false) +} + +func WrapAuthenticatedWithUser(h HandlerFunc, l *zap.Logger) echo.HandlerFunc { + return authenticated(h, l, true, false, false) +} + +func WrapAuthenticatedWithCurrentWorkspace(h HandlerFunc, l *zap.Logger) echo.HandlerFunc { + return authenticated(h, l, false, true, false) +} + +func WrapAuthenticatedWithUserAllWorkspaces(h HandlerFunc, l *zap.Logger) echo.HandlerFunc { + return authenticated(h, l, true, false, true) +} + +func authenticated(h HandlerFunc, l *zap.Logger, withUser, withCurrentWorkspace, withAllWorkspaces bool) echo.HandlerFunc { + return func(c echo.Context) error { + rid := c.Response().Header().Get(echo.HeaderXRequestID) + + ctx := Context{ + Context: c, + L: l.With(zap.String("request_id", rid)), + } + + now := timecop.Now().Unix() + + tkCookie, err := c.Request().Cookie("session_token") + if err != nil && errors.Is(err, http.ErrNoCookie) { + return ctx.Unauthorized() + } + + rctx := c.Request().Context() + session, err := auth.FetchSessionByToken(rctx, tkCookie.Value) + + if err != nil || session.Expires < now { + return ctx.Unauthorized() + } + + ctx.Session = session + + if withUser { + ctx.User, err = auth.FetchUserByID(rctx, session.UserID) + if err != nil { + if errors.Is(err, auth.ErrUserNotFound) { + return ctx.Unauthorized() + } + + return ctx.InternalError(err) + } + } + + if withAllWorkspaces { + ctx.AllWorkspaces, err = auth.FetchWorkspacesForUser(rctx, ctx.Session.UserID) + if err != nil { + return ctx.InternalError(err) + } + } + + var wid int64 + echo.PathParamsBinder(c).Int64("workspace_id", &wid) + if wid > 0 { + ctx.WorkspaceID = wid + + if withAllWorkspaces { + ctx.AllWorkspaces, err = auth.FetchWorkspacesForUser(rctx, ctx.Session.UserID) + if err != nil { + return ctx.InternalError(err) + } + } + + // make sure user id part of the ws + var found bool + for _, ws := range ctx.AllWorkspaces { + if ws.ID == wid { + found = true + ctx.Workspace = ws + break + } + } + + if withCurrentWorkspace && ctx.Workspace == nil { + ctx.Workspace, err = auth.FetchWorkspaceByID(rctx, wid) + if err != nil { + if errors.Is(err, auth.ErrWorkspaceNotFound) { + return ctx.JSON(http.StatusNotFound, map[string]any{ + "error": "Workspace not found", + }) + } + + return ctx.InternalError(err) + } + } + + if !found { + return ctx.Unauthorized() + } + } + + return h(ctx) + } +}