Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add user management #117

Merged
merged 7 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: [] })
}
}
})
Loading