Skip to content

Commit

Permalink
feature: introduce locked tenant status (#700)
Browse files Browse the repository at this point in the history
  • Loading branch information
goenning committed Dec 26, 2018
1 parent fca5ff9 commit 2f8ffc2
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 23 deletions.
3 changes: 3 additions & 0 deletions app/cmd/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ func routes(r *web.Engine) *web.Engine {
r.Post("/_api/signin/complete", handlers.CompleteSignInProfile())
r.Post("/_api/signin", handlers.SignInByEmail())

//Block if it's a locked tenant with a non-administrator user
r.Use(middlewares.BlockLockedTenants())

//Block if it's private tenant with unauthenticated user
r.Use(middlewares.CheckTenantPrivacy())

Expand Down
15 changes: 8 additions & 7 deletions app/handlers/signin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ import (
"github.com/getfider/fider/app/models"
"github.com/getfider/fider/app/pkg/errors"
"github.com/getfider/fider/app/pkg/web"
"github.com/getfider/fider/app/pkg/web/util"
webutil "github.com/getfider/fider/app/pkg/web/util"
"github.com/getfider/fider/app/tasks"
)

// SignInPage renders the sign in page
func SignInPage() web.HandlerFunc {
return func(c web.Context) error {
if c.IsAuthenticated() || !c.Tenant().IsPrivate {
return c.Redirect(c.BaseURL())

if c.Tenant().IsPrivate || c.Tenant().Status == models.TenantLocked {
return c.Page(web.Props{
Title: "Sign in",
ChunkName: "SignIn.page",
})
}

return c.Page(web.Props{
Title: "Sign in",
ChunkName: "SignIn.page",
})
return c.Redirect(c.BaseURL())
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/handlers/signin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func TestVerifySignInKeyHandler_PrivateTenant_InviteRequest_NewUser(t *testing.T
Expect(code).Equals(http.StatusOK)
}

func TestVerifySignUpKeyHandler_InactiveTenant(t *testing.T) {
func TestVerifySignUpKeyHandler_PendingTenant(t *testing.T) {
RegisterT(t)

server, services := mock.NewServer()
Expand Down
19 changes: 19 additions & 0 deletions app/middlewares/tenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,22 @@ func CheckTenantPrivacy() web.MiddlewareFunc {
}
}
}

// BlockLockedTenants blocks requests of non-administrator users on locked tenants
func BlockLockedTenants() web.MiddlewareFunc {
return func(next web.HandlerFunc) web.HandlerFunc {
return func(c web.Context) error {
if c.Tenant().Status == models.TenantLocked {
if c.Request.IsAPI() {
return c.JSON(http.StatusLocked, web.Map{})
}

isAdmin := c.IsAuthenticated() && c.User().Role == models.RoleAdministrator
if !isAdmin {
return c.Redirect("/signin")
}
}
return next(c)
}
}
}
49 changes: 49 additions & 0 deletions app/middlewares/tenant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,52 @@ func TestRequireTenant_SingleHostMode_ValidTenant(t *testing.T) {
Expect(status).Equals(http.StatusOK)
Expect(response.Body.String()).Equals("Demonstration")
}

func TestBlockLockedTenants_ActiveTenant(t *testing.T) {
RegisterT(t)
server, _ := mock.NewServer()
server.Use(middlewares.BlockLockedTenants())

status, response := server.
WithURL("http://demo.test.fider.io").
OnTenant(mock.DemoTenant).
Execute(func(c web.Context) error {
return c.String(http.StatusOK, c.Tenant().Name)
})

Expect(status).Equals(http.StatusOK)
Expect(response.Body.String()).Equals("Demonstration")
}

func TestBlockLockedTenants_LockedTenant(t *testing.T) {
RegisterT(t)
server, _ := mock.NewServer()
server.Use(middlewares.BlockLockedTenants())
mock.DemoTenant.Status = models.TenantLocked

status, response := server.
WithURL("http://demo.test.fider.io").
OnTenant(mock.DemoTenant).
Execute(func(c web.Context) error {
return c.String(http.StatusOK, c.Tenant().Name)
})

Expect(status).Equals(http.StatusTemporaryRedirect)
Expect(response.HeaderMap.Get("Location")).Equals("/signin")
}

func TestBlockLockedTenants_LockedTenant_APICall(t *testing.T) {
RegisterT(t)
server, _ := mock.NewServer()
server.Use(middlewares.BlockLockedTenants())
mock.DemoTenant.Status = models.TenantLocked

status, _ := server.
WithURL("http://demo.test.fider.io/api/v1/posts").
OnTenant(mock.DemoTenant).
Execute(func(c web.Context) error {
return c.String(http.StatusOK, c.Tenant().Name)
})

Expect(status).Equals(http.StatusLocked)
}
4 changes: 2 additions & 2 deletions app/models/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ var (
TenantActive = 1
//TenantPending is used for signup via email that requires user confirmation
TenantPending = 2
//TenantInactive is used when tenants are inative for various reasons
TenantInactive = 3
//TenantLocked is used when tenants are locked for various reasons
TenantLocked = 3
)

//Upload represents a file that has been uploaded to Fider
Expand Down
24 changes: 20 additions & 4 deletions public/pages/SignIn/SignIn.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ import React from "react";
import { SignInControl, TenantLogo, LegalNotice } from "@fider/components";
import { notify, Fider } from "@fider/services";

const messages = {
locked: () => (
<>
<p className="welcome">
<strong>{Fider.session.tenant.name}</strong> is currently locked.
</p>
<p>To reactivate this site, sign in with an administrator account and update the required settings.</p>
</>
),
private: () => (
<>
<p className="welcome">
<strong>{Fider.session.tenant.name}</strong> is a private space and requires an invitation to join it.
</p>
<p>If you have an account or an invitation, you may use following options to sign in.</p>
</>
)
};

export default class SignInPage extends React.Component<{}, {}> {
private onEmailSent = (email: string) => {
notify.success(
Expand All @@ -18,10 +37,7 @@ export default class SignInPage extends React.Component<{}, {}> {
<div id="p-signin" className="page container">
<div className="message">
<TenantLogo size={100} />
<p className="welcome">
<strong>{Fider.session.tenant.name}</strong> is a private space and requires an invitation to join it.
</p>
<p>If you have an account or an invitation, you may use following options to sign in.</p>
{Fider.session.tenant.isPrivate ? messages.private() : messages.locked()}
</div>
<SignInControl onEmailSent={this.onEmailSent} useEmail={true} redirectTo={Fider.settings.baseURL} />
<LegalNotice />
Expand Down
27 changes: 18 additions & 9 deletions public/services/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { analytics } from "@fider/services";
import { analytics, notify } from "@fider/services";

export interface ErrorItem {
field?: string;
Expand All @@ -17,20 +17,29 @@ export interface Result<T = void> {

async function toResult<T>(response: Response): Promise<Result<T>> {
const body = await response.json();

if (response.status < 400) {
return {
ok: true,
data: body as T
};
} else {
return {
ok: false,
data: body as T,
error: {
errors: body.errors
}
};
}

if (response.status === 500) {
notify.error("An unexpected error occurred while processing your request.");
} else if (response.status === 403) {
notify.error("You are not authorized to perform this operation.");
} else if (response.status === 423) {
notify.error("This operation is not allowed. Update your billing settings to unlock it.");
}

return {
ok: false,
data: body as T,
error: {
errors: body.errors
}
};
}
async function request<T>(url: string, method: "GET" | "POST" | "PUT" | "DELETE", body?: any): Promise<Result<T>> {
const headers = [["Accept", "application/json"], ["Content-Type", "application/json"]];
Expand Down
1 change: 1 addition & 0 deletions scripts/kill-dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kill $(lsof -t -i :3000)

0 comments on commit 2f8ffc2

Please sign in to comment.