logout button

This commit is contained in:
Martin McKeaveney 2021-04-13 13:56:28 +01:00
commit bd48b02ab1
15 changed files with 201 additions and 176 deletions

View File

@ -7,7 +7,7 @@ const { StaticDatabases } = require("./db/utils")
const { jwt, local, google, authenticated } = require("./middleware") const { jwt, local, google, authenticated } = require("./middleware")
const { Cookies, UserStatus } = require("./constants") const { Cookies, UserStatus } = require("./constants")
const { hash, compare } = require("./hashing") const { hash, compare } = require("./hashing")
const { getAppId, setCookie, getCookie } = require("./utils") const { getAppId, setCookie, getCookie, clearCookie } = require("./utils")
const { const {
generateUserID, generateUserID,
getUserParams, getUserParams,
@ -46,5 +46,6 @@ module.exports = {
getAppId, getAppId,
setCookie, setCookie,
getCookie, getCookie,
clearCookie,
authenticated, authenticated,
} }

View File

@ -2,10 +2,7 @@ import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store" import { get } from "svelte/store"
import { findComponent, findComponentPath } from "./storeUtils" import { findComponent, findComponentPath } from "./storeUtils"
import { store } from "builderStore" import { store } from "builderStore"
import { import { tables as tablesStore, queries as queriesStores } from "stores/backend"
tables as tablesStore,
queries as queriesStores,
} from "stores/backend"
import { makePropSafe } from "@budibase/string-templates" import { makePropSafe } from "@budibase/string-templates"
import { TableNames } from "../constants" import { TableNames } from "../constants"

View File

@ -0,0 +1,28 @@
<script>
import { TextButton as Button, Modal } from "@budibase/bbui"
import { auth } from "stores/backend"
</script>
<div>
<Button text on:click={auth.logout}>
<i class="ri-logout-box-line" />
<p>Logout</p>
</Button>
</div>
<style>
div i {
font-size: 26px;
color: var(--grey-7);
margin-left: 12px;
margin-top: 10px;
}
div p {
font-family: var(--font-sans);
font-size: var(--font-size-s);
color: var(--ink);
font-weight: 400;
margin: 0 0 0 12px;
}
</style>

View File

@ -11,9 +11,7 @@
{#if $datasources.list.length === 0} {#if $datasources.list.length === 0}
<i>Connect your first datasource to start building.</i> <i>Connect your first datasource to start building.</i>
{:else} {:else}<i>Select a datasource to edit</i>{/if}
<i>Select a datasource to edit</i>
{/if}
<style> <style>
i { i {

View File

@ -1,5 +1,5 @@
<script> <script>
import { Home as Link } from "@budibase/bbui" import { Home as Link, Button } from "@budibase/bbui"
import { import {
AppsIcon, AppsIcon,
HostingIcon, HostingIcon,
@ -9,6 +9,7 @@
} from "components/common/Icons" } from "components/common/Icons"
import LoginForm from "components/login/LoginForm.svelte" import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte" import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg" import Logo from "/assets/budibase-logo.svg"
import { auth } from "stores/backend" import { auth } from "stores/backend"
@ -44,6 +45,7 @@
</div> </div>
<div class="nav-bottom"> <div class="nav-bottom">
<BuilderSettingsButton /> <BuilderSettingsButton />
<LogoutButton />
</div> </div>
</div> </div>
</div> </div>

View File

@ -25,7 +25,7 @@ export function createAuthStore() {
return json return json
}, },
logout: async () => { logout: async () => {
const response = await api.post(`/api/auth/logout`) const response = await api.post(`/api/admin/auth/logout`)
const json = await response.json() const json = await response.json()
set({ user: null }) set({ user: null })
}, },

View File

@ -32,12 +32,15 @@ module.exports = async (ctx, next) => {
updateCookie = true updateCookie = true
appId = requestAppId appId = requestAppId
roleId = BUILTIN_ROLE_IDS.PUBLIC roleId = BUILTIN_ROLE_IDS.PUBLIC
} else if (requestAppId != null) { } else if (
requestAppId != null &&
(appCookie == null || requestAppId !== appCookie.appId)
) {
const globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user.email) const globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user.email)
updateCookie = true updateCookie = true
appId = requestAppId appId = requestAppId
roleId = globalUser.roles[requestAppId] || BUILTIN_ROLE_IDS.PUBLIC roleId = globalUser.roles[requestAppId] || BUILTIN_ROLE_IDS.PUBLIC
} else if (requestAppId == null && appCookie != null) { } else if (appCookie != null) {
appId = appCookie.appId appId = appCookie.appId
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC
} }

View File

@ -1,26 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Authenticated middleware sets the correct APP auth type information when the user is not in the builder 1`] = `
Object {
"apiKey": "1234",
"role": Role {
"_id": "ADMIN",
"inherits": "POWER",
"name": "Admin",
"permissionId": "admin",
},
"roleId": "ADMIN",
}
`;
exports[`Authenticated middleware sets the correct BUILDER auth type information when the x-budibase-type header is not 'client' 1`] = `
Object {
"apiKey": "1234",
"role": Role {
"_id": "BUILDER",
"name": "Builder",
"permissionId": "admin",
},
"roleId": "BUILDER",
}
`;

View File

@ -1,124 +0,0 @@
const { AuthTypes } = require("../../constants")
const authenticatedMiddleware = require("../authenticated")
const jwt = require("jsonwebtoken")
jest.mock("jsonwebtoken")
class TestConfiguration {
constructor(middleware) {
this.middleware = authenticatedMiddleware
this.ctx = {
config: {},
auth: {},
cookies: {
set: jest.fn(),
get: jest.fn(),
},
headers: {},
params: {},
path: "",
request: {
headers: {},
},
throw: jest.fn(),
}
this.next = jest.fn()
}
setHeaders(headers) {
this.ctx.headers = headers
}
executeMiddleware() {
return this.middleware(this.ctx, this.next)
}
afterEach() {
jest.resetAllMocks()
}
}
describe("Authenticated middleware", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
config.afterEach()
})
it("calls next() when on the builder path", async () => {
config.ctx.path = "/builder"
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
})
it("sets a new cookie when the current cookie does not match the app id from context", async () => {
const appId = "app_123"
config.setHeaders({
"x-budibase-app-id": appId,
})
config.ctx.cookies.get.mockImplementation(() => "cookieAppId")
await config.executeMiddleware()
expect(config.ctx.cookies.set).toHaveBeenCalledWith(
"budibase:currentapp",
appId,
expect.any(Object)
)
})
it("sets the correct BUILDER auth type information when the x-budibase-type header is not 'client'", async () => {
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
jwt.verify.mockImplementationOnce(() => ({
apiKey: "1234",
roleId: "BUILDER",
}))
await config.executeMiddleware()
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER)
expect(config.ctx.user).toMatchSnapshot()
})
it("sets the correct APP auth type information when the user is not in the builder", async () => {
config.setHeaders({
"x-budibase-type": "client",
})
config.ctx.cookies.get.mockImplementation(() => `budibase:app:local`)
jwt.verify.mockImplementationOnce(() => ({
apiKey: "1234",
roleId: "ADMIN",
}))
await config.executeMiddleware()
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP)
expect(config.ctx.user).toMatchSnapshot()
})
it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => {
config.executeMiddleware()
expect(config.ctx.auth.authenticated).toBe(false)
expect(config.ctx.user.role).toEqual({
_id: "PUBLIC",
name: "Public",
permissionId: "public",
})
})
it("clears the cookie when there is an error authenticating in the builder", async () => {
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
jwt.verify.mockImplementationOnce(() => {
throw new Error()
})
await config.executeMiddleware()
expect(config.ctx.cookies.set).toBeCalledWith("budibase:builder:local")
})
})

View File

@ -1,6 +1,5 @@
const authorizedMiddleware = require("../authorized") const authorizedMiddleware = require("../authorized")
const env = require("../../environment") const env = require("../../environment")
const { AuthTypes } = require("../../constants")
const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions") const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions")
jest.mock("../../environment", () => ({ jest.mock("../../environment", () => ({
prod: false, prod: false,

View File

@ -0,0 +1,148 @@
mockAuthWithNoCookie()
mockWorker()
function mockWorker() {
jest.mock("../../utilities/workerRequests", () => ({
getGlobalUsers: () => {
return {
email: "test@test.com",
roles: {
"app_test": "BASIC",
}
}
}
}))
}
function mockReset() {
jest.resetModules()
mockWorker()
}
function mockAuthWithNoCookie() {
jest.resetModules()
mockWorker()
jest.mock("@budibase/auth", () => ({
getAppId: jest.fn(),
setCookie: jest.fn(),
getCookie: jest.fn(),
Cookies: {},
}))
}
function mockAuthWithCookie() {
jest.resetModules()
mockWorker()
jest.mock("@budibase/auth", () => ({
getAppId: () => {
return "app_test"
},
setCookie: jest.fn(),
getCookie: () => ({ appId: "app_different", roleId: "PUBLIC" }),
Cookies: {
Auth: "auth",
CurrentApp: "currentapp",
}
}))
}
class TestConfiguration {
constructor() {
this.next = jest.fn()
this.throw = jest.fn()
this.ctx = {
next: this.next,
throw: this.throw
}
}
setUser() {
this.ctx.user = {
email: "test@test.com",
}
}
executeMiddleware() {
// import as late as possible for mocks
const currentAppMiddleware = require("../currentapp")
return currentAppMiddleware(this.ctx, this.next)
}
}
describe("Current app middleware", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("test having no cookies or app ID", () => {
it("should be able to proceed with nothing setup", async () => {
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
})
})
describe("check get public for app when not logged in", () => {
it("should be able to proceed with no login, but cookies configured", async () => {
mockAuthWithCookie()
await config.executeMiddleware()
expect(config.ctx.roleId).toEqual("PUBLIC")
expect(config.ctx.appId).toEqual("app_test")
expect(config.next).toHaveBeenCalled()
})
})
describe("check functionality when logged in", () => {
async function checkExpected(setCookie) {
config.setUser()
await config.executeMiddleware()
const cookieFn = require("@budibase/auth").setCookie
if (setCookie) {
expect(cookieFn).toHaveBeenCalled()
} else {
expect(cookieFn).not.toHaveBeenCalled()
}
expect(config.ctx.roleId).toEqual("BASIC")
expect(config.ctx.appId).toEqual("app_test")
expect(config.next).toHaveBeenCalled()
}
it("should be able to setup an app token when cookie not setup", async () => {
mockAuthWithCookie()
await checkExpected(true)
})
it("should perform correct when no cookie exists", async () => {
mockReset()
jest.mock("@budibase/auth", () => ({
getAppId: () => {
return "app_test"
},
setCookie: jest.fn(),
getCookie: jest.fn(),
Cookies: {},
}))
await checkExpected(true)
})
it("lastly check what occurs when cookie doesn't need updated", async () => {
mockReset()
jest.mock("@budibase/auth", () => ({
getAppId: () => {
return "app_test"
},
setCookie: jest.fn(),
getCookie: () => ({ appId: "app_test", roleId: "BASIC" }),
Cookies: {},
}))
await checkExpected(false)
})
})
})

View File

@ -1,4 +1,4 @@
const { passport, Cookies } = require("@budibase/auth") const { passport, Cookies, clearCookie } = require("@budibase/auth")
exports.authenticate = async (ctx, next) => { exports.authenticate = async (ctx, next) => {
return passport.authenticate("local", async (err, user) => { return passport.authenticate("local", async (err, user) => {
@ -27,6 +27,15 @@ exports.authenticate = async (ctx, next) => {
})(ctx, next) })(ctx, next)
} }
exports.logout = async ctx => {
clearCookie(Cookies.Auth)
ctx.body = { success: true }
}
exports.googleAuth = async (ctx, next) => {
// return passport.authenticate("google")
}
exports.googleAuth = async (ctx, next) => { exports.googleAuth = async (ctx, next) => {
// return passport.authenticate("google") // return passport.authenticate("google")
} }

View File

@ -6,6 +6,7 @@ const router = Router()
router router
.post("/api/admin/auth", authController.authenticate) .post("/api/admin/auth", authController.authenticate)
.post("/api/admin/auth/logout", authController.logout)
.get("/api/auth/google", passport.authenticate("google")) .get("/api/auth/google", passport.authenticate("google"))
.get( .get(
"/api/auth/google/callback", "/api/auth/google/callback",

View File

@ -1,11 +0,0 @@
const env = require("../environment")
module.exports = async (ctx, next) => {
const selfHostKey =
ctx.request.headers["x-budibase-auth"] || ctx.request.body.selfHostKey
if (!selfHostKey || env.SELF_HOST_KEY !== selfHostKey) {
ctx.throw(401, "Request unauthorised")
} else {
await next()
}
}