Skip to content

Commit

Permalink
feat: Add user management (#117)
Browse files Browse the repository at this point in the history
* Add or update the Azure App Service build and deployment workflow config

* refactor: Merge deployment YAMLs

* fix: update secret

* feat: add user management

Remove comment

* refactor: improve user handling

* Remove tests relying on SQL due to failure to mock them

* disable playwright
  • Loading branch information
ReinderVosDeWael authored Jul 9, 2024
1 parent 1cd6f76 commit 695f5a0
Show file tree
Hide file tree
Showing 20 changed files with 133 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
push:
branches:
- main
- development
workflow_dispatch:

env:
Expand All @@ -28,7 +29,6 @@ jobs:
- name: npm install, build, and test
run: |
npm install
npx playwright install --with-deps
npm run build --if-present
npm run test --if-present
Expand All @@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
environment:
name: "Production"
name: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }}
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

steps:
Expand All @@ -62,6 +62,6 @@ jobs:
uses: azure/webapps-deploy@v3
with:
app-name: "cliniciantoolkit"
slot-name: "Production"
slot-name: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }}
package: .
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_9A3AB102193241559E3EADB479BA0400 }}
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D8F19C70B5EA48B29386C08288419993 }}
1 change: 0 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ jobs:
- name: Install dependencies
run: |
npm install
npx playwright install --with-deps
- name: Run tests
run: |
npm run test
Expand Down
12 changes: 0 additions & 12 deletions integration_tests/navbar.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "npm run test:integration && npm run test:unit",
"test": "npm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check 'src/**/*.{js,ts}' && eslint 'src/**/*.{js,ts}'",
Expand Down
37 changes: 0 additions & 37 deletions src/hooks.server.test.ts

This file was deleted.

59 changes: 55 additions & 4 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import { logger } from "$lib/server/logging"
import { randomUUID } from "crypto"
import { performance } from "perf_hooks"
import { pool } from "$lib/server/sql"
import { DEVELOPMENT_USER } from "$lib/server/environment"
import type { User } from "$lib/databaseTypes"
import type { RequestEvent } from "@sveltejs/kit"

const ADMIN_ENDPOINTS = ["/api/templates/.*?"]

export async function handle({ event, resolve }) {
const requestId = randomUUID()
const startTime = performance.now()

const user = event.request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") || "[email protected]"
const userEmail = event.request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") || DEVELOPMENT_USER
logger.info({
type: `Request`,
method: event.request.method,
url: event.request.url,
user,
user: userEmail,
requestId
})
event.request.headers.set("X-Request-ID", requestId)
event.request.headers.set("X-User", user)
event.request.headers.set("X-User", userEmail)

const user = await getOrInsertUser(userEmail)
if (!isUserAuthorized(event, user)) {
const endTime = performance.now()
const responseTime = `${(endTime - startTime).toFixed(3)}ms`
logger.error({
type: "Unauthorized",
method: event.request.method,
url: event.request.url,
user: userEmail,
requestId,
responseTime
})
return new Response("Unauthorized", { status: 401 })
}

const response = await resolve(event)

Expand All @@ -26,7 +47,7 @@ export async function handle({ event, resolve }) {
statusCode: response.status,
method: event.request.method,
url: event.request.url,
user,
user: userEmail,
requestId,
responseTime
}
Expand All @@ -41,3 +62,33 @@ export async function handle({ event, resolve }) {

return response
}

// Exported for testing purposes.
export async function getOrInsertUser(email: string) {
return await pool.connect().then(async client => {
const getUserQuery = {
text: "SELECT * FROM users WHERE email = $1",
values: [email]
}
const getResult = await client.query(getUserQuery)
if (getResult.rows.length > 0) {
client.release()
return getResult.rows[0] as User
}

const insertUserQuery = {
text: "INSERT INTO users (email) VALUES ($1) RETURNING *",
values: [email]
}
const insertResult = await client.query(insertUserQuery)
client.release()
return insertResult.rows[0] as User
})
}

function isUserAuthorized(event: RequestEvent<Partial<Record<string, string>>, string | null>, user: User) {
if (!user.is_admin && ADMIN_ENDPOINTS.some(endpoint => event.request.url.match(endpoint))) {
return false
}
return true
}
6 changes: 6 additions & 0 deletions src/lib/databaseTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type User = {
id: number
email: string
is_admin: boolean
is_alpha_user: boolean
}
2 changes: 1 addition & 1 deletion src/lib/server/azure.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AZURE_BLOB_ACCOUNT_NAME, AZURE_BLOB_SAS } from "./secrets"
import { AZURE_BLOB_ACCOUNT_NAME, AZURE_BLOB_SAS } from "./environment"
import { logger } from "./logging"

if (!AZURE_BLOB_ACCOUNT_NAME || !AZURE_BLOB_SAS) {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/server/secrets.ts → src/lib/server/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export const AZURE_FUNCTION_PYTHON_KEY = env.AZURE_FUNCTION_PYTHON_KEY

export const AZURE_BLOB_ACCOUNT_NAME = env.AZURE_BLOB_ACCOUNT_NAME
export const AZURE_BLOB_SAS = env.AZURE_BLOB_SAS

export const DEVELOPMENT_USER = env.DEVELOPMENT_USER
2 changes: 1 addition & 1 deletion src/lib/server/sql.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pkg from "pg"
import { POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_PORT } from "./secrets"
import { POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_PORT } from "./environment"

const { Pool } = pkg
const config = {
Expand Down
25 changes: 25 additions & 0 deletions src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { User } from "$lib/databaseTypes"
import { DEVELOPMENT_USER } from "$lib/server/environment.js"
import { pool } from "$lib/server/sql"

export async function load({ request }) {
const userEmail = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") || DEVELOPMENT_USER
const userQuery = {
text: "SELECT * FROM users WHERE email = $1",
values: [userEmail]
}
const user: User = await pool.connect().then(async client => {
const result = await client.query(userQuery)
client.release()
return result.rows[0]
})

if (!user) {
return {
status: 401,
error: "User not found."
}
}

return { user }
}
2 changes: 1 addition & 1 deletion src/routes/api/health/+server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from "$lib/server/logging"
import { AZURE_FUNCTION_PYTHON_KEY, AZURE_FUNCTION_PYTHON_URL } from "$lib/server/secrets"
import { AZURE_FUNCTION_PYTHON_KEY, AZURE_FUNCTION_PYTHON_URL } from "$lib/server/environment"

export async function GET() {
logger.info("Warming up the server.")
Expand Down
2 changes: 1 addition & 1 deletion src/routes/api/intake-report/[id]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from "$lib/server/logging"
import { AZURE_FUNCTION_PYTHON_KEY, AZURE_FUNCTION_PYTHON_URL } from "$lib/server/secrets"
import { AZURE_FUNCTION_PYTHON_KEY, AZURE_FUNCTION_PYTHON_URL } from "$lib/server/environment"

export async function GET({ params, fetch }) {
const id = params.id
Expand Down
2 changes: 1 addition & 1 deletion src/routes/api/llm/+server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { env } from "$env/dynamic/private"
import { logger } from "$lib/server/logging"
import { AZURE_FUNCTION_PYTHON_KEY } from "$lib/server/secrets"
import { AZURE_FUNCTION_PYTHON_KEY } from "$lib/server/environment"

export async function POST({ fetch, request }) {
logger.info("Making LLM request.")
Expand Down
2 changes: 1 addition & 1 deletion src/routes/api/markdown2docx/+server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from "$lib/server/logging"
import { AZURE_FUNCTION_PYTHON_KEY, AZURE_FUNCTION_PYTHON_URL } from "$lib/server/secrets"
import { AZURE_FUNCTION_PYTHON_KEY, AZURE_FUNCTION_PYTHON_URL } from "$lib/server/environment"

export async function POST({ fetch, request }) {
logger.info("Converting markdown to docx")
Expand Down
25 changes: 0 additions & 25 deletions src/routes/api/templates/[id]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,6 @@ export async function POST({ request, params }) {
})
}

export async function GET({ params }) {
const id = params.id
logger.info(`Getting template with id ${id}`)

const query = {
text: "SELECT * FROM templates WHERE id = $1",
values: [id]
}

return await pool
.connect()
.then(async client => {
const result = await client.query(query)
client.release()
return result.rows[0]
})
.then(row => {
return new Response(JSON.stringify(row), { headers: { "Content-Type": "application/json" } })
})
.catch(error => {
logger.error(`Error getting template with id ${id}:`, error)
return new Response(null, { status: 500 })
})
}

export async function PATCH({ params, request }) {
const id = params.id
let { text, parent_id } = await request.json()
Expand Down
10 changes: 7 additions & 3 deletions src/routes/templates/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import SelectedNodes from "./SelectedNodes.svelte"
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte"
export let data
let selectedNodes: DecisionTree[] = []
let tabSet: number = 0
let editable: boolean = false
Expand Down Expand Up @@ -44,9 +46,11 @@

<svelte:fragment slot="panel">
<div hidden={tabSet !== 0}>
<div class="right-0">
<SlideToggle name="slider-editable" size="sm" bind:checked={editable}>Editable</SlideToggle>
</div>
{#if data.user?.is_admin}
<div class="right-0">
<SlideToggle name="slider-editable" size="sm" bind:checked={editable}>Editable</SlideToggle>
</div>
{/if}
<TemplatesDirectory {nodes} bind:selectedNodes {editable} />
</div>
<div hidden={tabSet !== 1}>
Expand Down
24 changes: 19 additions & 5 deletions src/routes/templates/TemplatesDirectory/AdminButtons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,19 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: response.value })
})
.then(res => res.json())
.then(res => {
if (!res.ok) {
toastStore.trigger({
message: `Failed to create the template: ${res.statusText}`,
background: "variant-filled-error"
})
} else {
return res.json()
}
})
.then(newNode => {
node.children = node.children.concat(new DecisionTree([newNode], newNode.id, node))
})
.catch(console.error)
}
}
modalStore.trigger(modal)
Expand All @@ -79,9 +87,15 @@
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: response.value, parentId })
}).then(result => {
if (!result.ok) {
toastStore.trigger({
message: `Failed to edit the template: ${result.statusText}`,
background: "variant-filled-error"
})
}
node.text = response.value
})
.then(() => (node.text = response.value))
.catch(console.error)
}
}
modalStore.trigger(modal)
Expand All @@ -101,7 +115,7 @@
await fetch(`/api/templates/${node.id}`, { method: "DELETE" }).then(response => {
if (!response.ok) {
toastStore.trigger({
message: "Failed to delete the template.",
message: "Failed to delete the template: " + response.statusText,
background: "variant-filled-error"
})
} else if (!node.parent) {
Expand Down
1 change: 0 additions & 1 deletion src/tests/components/Loadingbar.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { cleanup, render } from "@testing-library/svelte"
import LoadingBar from "$lib/components/LoadingBar.svelte"

afterEach(cleanup)

describe("LoadingBar Component", () => {
Expand Down
8 changes: 8 additions & 0 deletions src/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
import "@testing-library/jest-dom"

vi.mock("lib/server/sql", async () => {
return {
pool: {
connect: vi.fn().mockResolvedValue({ rows: [] })
}
}
})

0 comments on commit 695f5a0

Please sign in to comment.