From f7dd955abb6ba36035346f820336810bc4ea902d Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Thu, 5 Sep 2024 10:33:51 +1000 Subject: [PATCH 01/34] Add redirect to login route Signed-off-by: Steve Cassidy --- api/src/auth_routes.ts | 64 ++++++++++++++++++++++++++++++++------ api/src/authkeys/create.ts | 16 ++++++++++ api/src/routes.ts | 25 ++++----------- api/views/auth.handlebars | 10 +++--- 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/api/src/auth_routes.ts b/api/src/auth_routes.ts index 76137d298..d1ede5669 100644 --- a/api/src/auth_routes.ts +++ b/api/src/auth_routes.ts @@ -28,6 +28,7 @@ import {registerLocalUser} from './auth_providers/local'; import {body, validationResult} from 'express-validator'; import {getInvite} from './couchdb/invites'; import {acceptInvite} from './registration'; +import {generateUserToken} from './authkeys/create'; const AVAILABLE_AUTH_PROVIDER_DISPLAY_INFO: {[name: string]: any} = { google: { @@ -63,9 +64,27 @@ export function determine_callback_urls(provider_name: string): { }; } +/** + * Check that a redirect URL is one that we allow + * + * @param redirect URL to redirect to + * @returns a valid URl to redirect to, default to '/' if + * the one passed in is bad + */ +function validateRedirect(redirect: string) { + if (redirect.startsWith('http')) { + // should match against a whitelist of allowed URLs + return redirect; + } else if (redirect.startsWith('/')) { + return redirect; + } else { + return '/'; + } +} + export function add_auth_routes(app: any, handlers: any) { app.get('/auth/', (req: any, res: any) => { - // Allow the user to decide what auth mechanism to use + const redirect = validateRedirect(req.query?.redirect || '/'); const available_provider_info = []; for (const handler of handlers) { available_provider_info.push({ @@ -77,17 +96,44 @@ export function add_auth_routes(app: any, handlers: any) { providers: available_provider_info, localAuth: true, // maybe make this configurable? messages: req.flash(), + redirect: redirect, }); }); - // handle local login post request - app.post( - '/auth/local', - passport.authenticate('local', { - successRedirect: '/send-token', - failureRedirect: '/auth', - }) - ); + // app.post('/auth/local', (req: any, res: any, next: any) => { + // const redirect = validateRedirect(req.query?.redirect || '/'); + // passport.authenticate('local', { + // successRedirect: redirect, + // failureRedirect: `/login?redirect=${redirect}`, + // })(req, res, next); + // }); + + app.post('/auth/local', (req: any, res: any, next: any) => { + const redirect = validateRedirect(req.query?.redirect || '/'); + passport.authenticate('local', (err: any, user: any, info: any) => { + if (err) { + return next(err); + } + if (!user) { + return res.status(401).json({message: 'Authentication failed'}); + } + + req.login(user, async (loginErr: any) => { + if (loginErr) { + return next(loginErr); + } + console.log('user', user); + // Generate a token + const token = await generateUserToken(user); + console.log('token', token); + // Append the token to the redirect URL + const redirectUrlWithToken = `${redirect}?token=${token.token}`; + + // Redirect to the app with the token + return res.redirect(redirectUrlWithToken); + }); + })(req, res, next); + }); // accept an invite, auth not required, we invite them to // register if they aren't already diff --git a/api/src/authkeys/create.ts b/api/src/authkeys/create.ts index 6dee0a87d..b692299bf 100644 --- a/api/src/authkeys/create.ts +++ b/api/src/authkeys/create.ts @@ -21,6 +21,7 @@ import {SignJWT} from 'jose'; import type {SigningKey} from '../services/keyService'; +import {KEY_SERVICE} from '../buildconfig'; export async function createAuthKey( user: Express.User, @@ -42,3 +43,18 @@ export async function createAuthKey( .sign(signingKey.privateKey); return jwt; } + +export async function generateUserToken(user: Express.User) { + const signingKey = await KEY_SERVICE.getSigningKey(); + if (signingKey === null || signingKey === undefined) { + throw new Error('No signing key is available, check configuration'); + } else { + const token = await createAuthKey(user, signingKey); + + return { + token: token, + pubkey: signingKey.publicKeyString, + pubalg: signingKey.alg, + }; + } +} diff --git a/api/src/routes.ts b/api/src/routes.ts index ed8f23fb8..c71564b96 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -55,8 +55,7 @@ import { getNotebooks, getRolesForNotebook, } from './couchdb/notebooks'; -import {createAuthKey} from './authkeys/create'; -import {getPublicUserDbURL} from './couchdb'; +import {generateUserToken} from './authkeys/create'; import {add_auth_providers} from './auth_providers'; import {add_auth_routes} from './auth_routes'; @@ -199,13 +198,7 @@ app.get('/', async (req, res) => { const provider = Object.keys(req.user.profiles)[0]; // BBS 20221101 Adding token to here so we can support copy from conductor const signingKey = await KEY_SERVICE.getSigningKey(); - const jwt_token = await createAuthKey(req.user, signingKey); - const token = { - jwt_token: jwt_token, - public_key: signingKey.publicKeyString, - alg: signingKey.alg, - userdb: getPublicUserDbURL(), // query: is this actually needed? - }; + const token = generateUserToken(req.user); if (signingKey === null || signingKey === undefined) { res.status(500).send('Signing key not set up'); } else { @@ -251,17 +244,11 @@ app.get('/send-token/', (req, res) => { app.get('/get-token/', async (req, res) => { if (req.user) { - const signingKey = await KEY_SERVICE.getSigningKey(); - if (signingKey === null || signingKey === undefined) { + try { + const token = await generateUserToken(req.user); + res.send(token); + } catch { res.status(500).send('Signing key not set up'); - } else { - const token = await createAuthKey(req.user, signingKey); - - res.send({ - token: token, - pubkey: signingKey.publicKeyString, - pubalg: signingKey.alg, - }); } } else { res.status(403).end(); diff --git a/api/views/auth.handlebars b/api/views/auth.handlebars index dd96a5acc..8f9fbcd3b 100644 --- a/api/views/auth.handlebars +++ b/api/views/auth.handlebars @@ -9,11 +9,11 @@

Sign In

Please select an authentication provider

- +
{{#each providers}} - {{ this.name }} {{/each}} @@ -26,14 +26,14 @@ {{#if localAuth}}

Local Login

- +
- +
- +
From 96386730c9e9bf1ac1b1babf9bd958004c3d0ed0 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Thu, 5 Sep 2024 11:09:16 +1000 Subject: [PATCH 02/34] add redirect logic to other auth handlers (untested) Signed-off-by: Steve Cassidy --- api/src/auth_routes.ts | 41 ++++++++++++++++++++++----------------- api/views/auth.handlebars | 4 +++- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/api/src/auth_routes.ts b/api/src/auth_routes.ts index d1ede5669..f4a396299 100644 --- a/api/src/auth_routes.ts +++ b/api/src/auth_routes.ts @@ -108,9 +108,13 @@ export function add_auth_routes(app: any, handlers: any) { // })(req, res, next); // }); - app.post('/auth/local', (req: any, res: any, next: any) => { - const redirect = validateRedirect(req.query?.redirect || '/'); - passport.authenticate('local', (err: any, user: any, info: any) => { + const authenticate_return = ( + req: any, + res: any, + next: any, + redirect: string + ) => { + return (err: any, user: any, info: any) => { if (err) { return next(err); } @@ -122,17 +126,23 @@ export function add_auth_routes(app: any, handlers: any) { if (loginErr) { return next(loginErr); } - console.log('user', user); // Generate a token const token = await generateUserToken(user); - console.log('token', token); // Append the token to the redirect URL const redirectUrlWithToken = `${redirect}?token=${token.token}`; // Redirect to the app with the token return res.redirect(redirectUrlWithToken); }); - })(req, res, next); + }; + }; + + app.post('/auth/local', (req: any, res: any, next: any) => { + const redirect = validateRedirect(req.query?.redirect || '/'); + passport.authenticate( + 'local', + authenticate_return(req, res, next, redirect) + )(req, res, next); }); // accept an invite, auth not required, we invite them to @@ -237,22 +247,17 @@ export function add_auth_routes(app: any, handlers: any) { // set up handlers for OAuth providers for (const handler of handlers) { - app.get(`/auth/${handler}/`, (req: any, res: any) => { + app.get(`/auth/${handler}/`, (req: any, res: any, next: any) => { + const redirect = validateRedirect(req.query?.redirect || '/'); if ( typeof req.query?.state === 'string' || typeof req.query?.state === 'undefined' ) { - passport.authenticate(handler + '-validate', HANDLER_OPTIONS[handler])( - req, - res, - (err?: {}) => { - // Hack to avoid users getting caught when they're not in the right - // groups. - console.error('Authentication Error', err); - // res.redirect('https://auth.datacentral.org.au/cas/logout'); - //throw err ?? Error('Authentication failed (next, no error)'); - } - ); + passport.authenticate( + handler + '-validate', + authenticate_return(req, res, next, redirect) + // HANDLER_OPTIONS[handler] + )(req, res, next); } else { throw Error( `state must be a string, or not set, not ${typeof req.query?.state}` diff --git a/api/views/auth.handlebars b/api/views/auth.handlebars index 8f9fbcd3b..d00df0bdf 100644 --- a/api/views/auth.handlebars +++ b/api/views/auth.handlebars @@ -9,11 +9,13 @@

Sign In

Please select an authentication provider

+ +

Will redirect to: {{redirect}}

{{#each providers}} - {{ this.name }} {{/each}} From 48d9022f1e8288cc76ce860c1304b8caae42b266 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Thu, 5 Sep 2024 15:02:38 +1000 Subject: [PATCH 03/34] package update Signed-off-by: Steve Cassidy --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f50478d33..58660c9dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "@faims3/data-model": "*", "archiver": "^6.0.1", "aws-sdk": "^2.1664.0", - "axios": "^1.7.4", + "axios": "^1.7.6", "body-parser": "1.20.2", "cache-manager": "^5.7.3", "cookie-session": "2.0.0", @@ -16083,9 +16083,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", From 78c78151a9a87a47b04620a6104a40eedbe35898 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Thu, 5 Sep 2024 15:03:37 +1000 Subject: [PATCH 04/34] First pass at auth return and removing pubkey use Signed-off-by: Steve Cassidy --- api/src/authkeys/create.ts | 3 +- app/src/App.tsx | 9 +++ app/src/constants/routes.tsx | 1 + .../components/authentication/auth_return.tsx | 76 +++++++++++++++++++ .../components/authentication/login_form.tsx | 3 + app/src/sync/databases.ts | 2 - app/src/users.ts | 61 ++++----------- 7 files changed, 104 insertions(+), 51 deletions(-) create mode 100644 app/src/gui/components/authentication/auth_return.tsx diff --git a/api/src/authkeys/create.ts b/api/src/authkeys/create.ts index b692299bf..3d0d06ab7 100644 --- a/api/src/authkeys/create.ts +++ b/api/src/authkeys/create.ts @@ -21,7 +21,7 @@ import {SignJWT} from 'jose'; import type {SigningKey} from '../services/keyService'; -import {KEY_SERVICE} from '../buildconfig'; +import {CONDUCTOR_PUBLIC_URL, KEY_SERVICE} from '../buildconfig'; export async function createAuthKey( user: Express.User, @@ -30,6 +30,7 @@ export async function createAuthKey( const jwt = await new SignJWT({ '_couchdb.roles': user.roles ?? [], name: user.name, + server: CONDUCTOR_PUBLIC_URL, }) .setProtectedHeader({ alg: signingKey.alg, diff --git a/app/src/App.tsx b/app/src/App.tsx index 0c0cac045..6ba0ad720 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -44,6 +44,7 @@ import {useEffect, useState} from 'react'; import {TokenContents} from '@faims3/data-model'; import NotFound404 from './gui/pages/404'; +import {AuthReturn} from './gui/components/authentication/auth_return'; // type AppProps = {}; @@ -82,6 +83,14 @@ export default function App() { } /> + + + + } + /> { + console.log('Received token via url for:', listing_id); + await setTokenForCluster(token, '', '', listing_id); + return listing_id; + }) + .then(async listing_id => { + console.log(listing_id); + // reprocess_listing(listing_id); + }) + .catch(err => { + console.warn('Failed to get token from url', err); + }); + return ( + <> +

Auth Token Returned

+
{JSON.stringify(token_obj, null, 2)}
+ + ); + } else { + return

Wassat?

; + } + } else { + navigate('/'); + } +} diff --git a/app/src/gui/components/authentication/login_form.tsx b/app/src/gui/components/authentication/login_form.tsx index b6b7cd77e..a3b31f93d 100644 --- a/app/src/gui/components/authentication/login_form.tsx +++ b/app/src/gui/components/authentication/login_form.tsx @@ -67,6 +67,9 @@ export function LoginButton(props: LoginButtonProps) { false ); if (await isWeb()) { + const redirect = `${window.location.protocol}//${window.location.host}/auth-return`; + window.location.href = + props.conductor_url + '/auth?redirect=' + redirect; // Open a new window/tab on web const oauth_window = window.open(props.conductor_url); if (oauth_window === null) { diff --git a/app/src/sync/databases.ts b/app/src/sync/databases.ts index 5f4058d6f..332b31b49 100644 --- a/app/src/sync/databases.ts +++ b/app/src/sync/databases.ts @@ -154,8 +154,6 @@ export const getLocalStateDB = () => { export type JWTToken = string; export interface JWTTokenInfo { - pubkey: string; - pubalg: string; token: JWTToken; } diff --git a/app/src/users.ts b/app/src/users.ts index 4aeb63b49..1b05f65a6 100644 --- a/app/src/users.ts +++ b/app/src/users.ts @@ -21,7 +21,7 @@ * on the happy path of not seeing access denied, or at least in ways the GUI * can meaningfully handle. */ -import {jwtVerify, KeyLike, importSPKI} from 'jose'; +import {jwtVerify, KeyLike, importSPKI, decodeJwt} from 'jose'; import {CLUSTER_ADMIN_GROUP_NAME} from './buildconfig'; import {LocalAuthDoc, JWTTokenMap, local_auth_db} from './sync/databases'; @@ -43,7 +43,6 @@ interface SplitCouchDBRole { interface TokenInfo { token: string; - pubkey: KeyLike; } /** @@ -70,20 +69,13 @@ export async function getCurrentUserId(project_id: ProjectID): Promise { /** * Store a token for a server (cluster) * @param token new authentication token - * @param pubkey token public key - * @param pubalg token pubkey algorithm * @param cluster_id server identifier that this token is for */ -export async function setTokenForCluster( - token: string, - pubkey: string, - pubalg: string, - cluster_id: string -) { +export async function setTokenForCluster(token: string, cluster_id: string) { if (token === undefined) throw Error('Token undefined in setTokenForCluster'); try { const doc = await local_auth_db.get(cluster_id); - const new_doc = await addTokenToDoc(token, pubkey, pubalg, cluster_id, doc); + const new_doc = await addTokenToDoc(token, cluster_id, doc); try { await local_auth_db.put(new_doc); @@ -98,7 +90,7 @@ export async function setTokenForCluster( } catch (err) { console.debug('Failed to get token when setting for', cluster_id, err); try { - const doc = await addTokenToDoc(token, pubkey, pubalg, cluster_id, null); + const doc = await addTokenToDoc(token, cluster_id, null); console.debug('Initial token info is:', doc); await local_auth_db.put(doc); } catch (err_initial: any) { @@ -111,26 +103,20 @@ export async function setTokenForCluster( /** * Add a token to an auth object or create a new one * @param token auth token - * @param pubkey public key - * @param pubalg pubkey algorithm * @param cluster_id server identifier * @param current_doc current auth doc if any * @returns a promise resolving to a new or updated auth document */ async function addTokenToDoc( token: string, - pubkey: string, - pubalg: string, cluster_id: string, current_doc: LocalAuthDoc | null ): Promise { - const new_username = await getUsernameFromToken(token, pubkey, pubalg); + const new_username = await getUsernameFromToken(token); if (current_doc === null) { const available_tokens: JWTTokenMap = {}; available_tokens[new_username] = { token, - pubkey, - pubalg, }; return { _id: cluster_id, @@ -141,8 +127,6 @@ async function addTokenToDoc( current_doc.current_username = new_username; current_doc.available_tokens[new_username] = { token, - pubkey, - pubalg, }; return current_doc; } @@ -225,8 +209,7 @@ export async function getAllUsersForCluster( const token_contents = []; const doc = await local_auth_db.get(cluster_id); for (const token_details of Object.values(doc.available_tokens)) { - const pubkey = await importSPKI(token_details.pubkey, token_details.pubalg); - token_contents.push(await parseToken(token_details.token, pubkey)); + token_contents.push(await parseToken(token_details.token)); } return token_contents; } @@ -240,13 +223,8 @@ export async function deleteAllTokensForCluster(cluster_id: string) { } } -async function getUsernameFromToken( - token: string, - pubkey: string, - pubalg: string -): Promise { - const keyobj = await importSPKI(pubkey, pubalg); - return (await parseToken(token, keyobj)).username; +async function getUsernameFromToken(token: string): Promise { + return (await parseToken(token)).username; } async function getTokenInfoForCluster( @@ -256,10 +234,8 @@ async function getTokenInfoForCluster( const doc = await local_auth_db.get(cluster_id); const username = doc.current_username; const token_details = doc.available_tokens[username]; - const pubkey = await importSPKI(token_details.pubkey, token_details.pubalg); return { token: token_details.token, - pubkey: pubkey, }; } catch (err) { return undefined; @@ -280,27 +256,16 @@ export async function getTokenContentsForCluster( return undefined; } try { - return await parseToken(token_info.token, token_info.pubkey); + return await parseToken(token_info.token); } catch (err: any) { - console.debug( - 'Failed to parse token', - token_info.token, - token_info.pubkey, - cluster_id - ); + console.debug('Failed to parse token', token_info.token, cluster_id); return undefined; } } -async function parseToken( - token: string, - pubkey: KeyLike -): Promise { - const res = await jwtVerify(token, pubkey); - const payload = res.payload; - // if (DEBUG_APP) { - // console.debug('Token payload is:', payload); - // } +async function parseToken(token: string): Promise { + const payload = await decodeJwt(token); + const username = payload.sub ?? undefined; if (username === undefined) { throw Error('Username not specified in token'); From e0d3284c0badc8b6e980e05af945f7e5d713fe36 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Thu, 5 Sep 2024 17:03:44 +1000 Subject: [PATCH 05/34] remove unused page and tests Signed-off-by: Steve Cassidy --- app/src/gui/pages/index.test.tsx | 70 ----------------- app/src/gui/pages/index.tsx | 128 ------------------------------- 2 files changed, 198 deletions(-) delete mode 100644 app/src/gui/pages/index.test.tsx delete mode 100644 app/src/gui/pages/index.tsx diff --git a/app/src/gui/pages/index.test.tsx b/app/src/gui/pages/index.test.tsx deleted file mode 100644 index 87d232c62..000000000 --- a/app/src/gui/pages/index.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2021, 2022 Macquarie University - * - * Licensed under the Apache License Version 2.0 (the, "License"); - * you may not use, this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing software - * distributed under the License is distributed on an "AS IS" BASIS - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. - * See, the License, for the specific language governing permissions and - * limitations under the License. - * - * Filename: index.tsx - * Description: - * TODO - */ - -import {render, screen} from '@testing-library/react'; -import {BrowserRouter as Router} from 'react-router-dom'; -import Index from '.'; -import {TokenContents} from '@faims3/data-model'; -import {expect, describe, vi, it} from 'vitest'; - -export function mockCheckToken(token: null | undefined | TokenContents) { - return token ? true : false; -} - -vi.mock('../../utils/helpers', () => ({ - checkToken: mockCheckToken, -})); - -const testToken = { - name: 'testData', - roles: ['testData'], - username: 'testData', -}; - -describe('Check index page', () => { - it('Check without token', async () => { - render( - - - - ); - expect(screen.getByText('Welcome')).toBeTruthy(); - - expect( - screen.getByText('Contact info@fieldmark.au for support.') - ).toBeTruthy(); - - expect(screen.getByText('Sign In')).toBeTruthy(); - }); - it('Check with token', async () => { - render( - - - - ); - expect(screen.getByText('Welcome')).toBeTruthy(); - - expect( - screen.getByText('Contact info@fieldmark.au for support.') - ).toBeTruthy(); - - expect(screen.getByText('Workspace')).toBeTruthy(); - }); -}); diff --git a/app/src/gui/pages/index.tsx b/app/src/gui/pages/index.tsx deleted file mode 100644 index 184f4173f..000000000 --- a/app/src/gui/pages/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2021, 2022 Macquarie University - * - * Licensed under the Apache License Version 2.0 (the, "License"); - * you may not use, this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing software - * distributed under the License is distributed on an "AS IS" BASIS - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. - * See, the License, for the specific language governing permissions and - * limitations under the License. - * - * Filename: index.tsx - * Description: - * TODO - */ - -import React, {useEffect} from 'react'; -import {NavLink} from 'react-router-dom'; - -import {Grid, Typography, Button} from '@mui/material'; -import * as ROUTES from '../../constants/routes'; -import {useTheme} from '@mui/material/styles'; -import {checkToken} from '../../utils/helpers'; -import {TokenContents} from '@faims3/data-model'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -type IndexProps = { - token?: null | undefined | TokenContents; -}; -export default function Index(props: IndexProps) { - /** - * Landing page - */ - const theme = useTheme(); - const isAuthenticated = checkToken(props.token); - useEffect(() => { - document.body.classList.add('bg-primary-gradient'); - - return () => { - document.body.classList.remove('bg-primary-gradient'); - }; - }); - - return ( - - - - - Welcome - - - Fieldmarkā„¢ is an open-source tool for born-digital field data - collection brought to you by the FAIMS Project. Supporting - electronic field notebooks by researchers, for researchers. - - - Contact info@fieldmark.au for support. - - - {isAuthenticated ? ( - - - - ) : ( - - - {/**/} - {/* Register your interest*/} - {/**/} - - )} - - - {/* picture could go here... */} - - - - ); -} From c7c8f7285bb67a2a47d8061055162b48157bb09c Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 09:14:17 +1000 Subject: [PATCH 06/34] Get login redirect working properly for local at least Signed-off-by: Steve Cassidy --- api/src/auth_routes.ts | 26 ++++--- app/src/App.tsx | 2 +- .../components/authentication/auth_return.tsx | 70 +++++++++++-------- app/src/sync/databases.ts | 2 - app/src/sync/process-initialization.ts | 2 - app/src/utils/helpers.tsx | 4 +- 6 files changed, 61 insertions(+), 45 deletions(-) diff --git a/api/src/auth_routes.ts b/api/src/auth_routes.ts index f4a396299..fd615fcf9 100644 --- a/api/src/auth_routes.ts +++ b/api/src/auth_routes.ts @@ -83,8 +83,13 @@ function validateRedirect(redirect: string) { } export function add_auth_routes(app: any, handlers: any) { - app.get('/auth/', (req: any, res: any) => { + app.get('/auth/', async (req: any, res: any) => { const redirect = validateRedirect(req.query?.redirect || '/'); + + if (req.user) { + await redirect_with_token(res, req.user, redirect); + } + const available_provider_info = []; for (const handler of handlers) { available_provider_info.push({ @@ -126,17 +131,22 @@ export function add_auth_routes(app: any, handlers: any) { if (loginErr) { return next(loginErr); } - // Generate a token - const token = await generateUserToken(user); - // Append the token to the redirect URL - const redirectUrlWithToken = `${redirect}?token=${token.token}`; - - // Redirect to the app with the token - return res.redirect(redirectUrlWithToken); + return redirect_with_token(res, user, redirect); }); }; }; + const redirect_with_token = async (res: any, user: Express.User, redirect: string) => { + // Generate a token + const token = await generateUserToken(user); + // Append the token to the redirect URL + const redirectUrlWithToken = `${redirect}?token=${token.token}`; + + // Redirect to the app with the token + return res.redirect(redirectUrlWithToken); + } + + app.post('/auth/local', (req: any, res: any, next: any) => { const redirect = validateRedirect(req.query?.redirect || '/'); passport.authenticate( diff --git a/app/src/App.tsx b/app/src/App.tsx index 3eedd27e2..79fdb0885 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -87,7 +87,7 @@ export default function App() { path={ROUTES.AUTH_RETURN} element={ - + } /> diff --git a/app/src/gui/components/authentication/auth_return.tsx b/app/src/gui/components/authentication/auth_return.tsx index 4b3b6e589..fad8fe7da 100644 --- a/app/src/gui/components/authentication/auth_return.tsx +++ b/app/src/gui/components/authentication/auth_return.tsx @@ -24,6 +24,9 @@ import {decodeJwt} from 'jose'; import {useNavigate} from 'react-router'; import {setTokenForCluster} from '../../../users'; import {getSyncableListingsInfo} from '../../../databaseAccess'; +import {useEffect} from 'react'; +import {TokenContents} from '@faims3/data-model'; +import {reprocess_listing} from '../../../sync/process-initialization'; async function getListingForConductorUrl(conductor_url: string) { const origin = new URL(conductor_url).origin; @@ -37,40 +40,47 @@ async function getListingForConductorUrl(conductor_url: string) { throw Error(`Unknown listing for conductor url ${conductor_url}`); } -export function AuthReturn() { +interface AuthReturnProps { + setToken: (token: TokenContents) => {}; +} + +export function AuthReturn(props: AuthReturnProps) { const navigate = useNavigate(); - const params = new URLSearchParams(window.location.search); - if (params.has('token')) { - const token = params.get('token'); + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.has('token')) { + const token = params.get('token'); - if (token) { - const token_obj = decodeJwt(decodeURIComponent(token)); + if (token) { + const token_obj = decodeJwt(decodeURIComponent(token)); - console.log('decoded', token_obj); - getListingForConductorUrl(token_obj.server) - .then(async listing_id => { - console.log('Received token via url for:', listing_id); - await setTokenForCluster(token, '', '', listing_id); - return listing_id; - }) - .then(async listing_id => { - console.log(listing_id); - // reprocess_listing(listing_id); - }) - .catch(err => { - console.warn('Failed to get token from url', err); - }); - return ( - <> -

Auth Token Returned

-
{JSON.stringify(token_obj, null, 2)}
- - ); + console.log('decoded', token_obj); + getListingForConductorUrl(token_obj.server as string) + .then(async listing_id => { + console.log('Received token via url for:', listing_id); + setTokenForCluster(token, listing_id).then(() => { + console.log('We have stored the token for ', listing_id); + reprocess_listing(listing_id); + // generate the TokenContents object like parseToken does + const token_content = { + username: token_obj.sub as string, + roles: (token_obj['_couchdb.roles'] as string[]) || '', + name: token_obj.name as string, + }; + console.log('%ctoken content', 'color: red', token_content); + props.setToken(token_content); + navigate('/'); + }); + }) + .catch(err => { + console.warn('Failed to get token from url', err); + }); + } } else { - return

Wassat?

; + navigate('/'); } - } else { - navigate('/'); - } + }, []); + + return

Auth Token

; } diff --git a/app/src/sync/databases.ts b/app/src/sync/databases.ts index 332b31b49..f415770ec 100644 --- a/app/src/sync/databases.ts +++ b/app/src/sync/databases.ts @@ -205,12 +205,10 @@ export function ensure_local_db( global_dbs: LocalDBList, start_sync_attachments: boolean ): [boolean, LocalDB] { - console.log('ensure_local_db', prefix, local_db_id, global_dbs); if (global_dbs[local_db_id]) { global_dbs[local_db_id].is_sync = start_sync; return [false, global_dbs[local_db_id]]; } else { - console.log('creating a new db', prefix, local_db_id); const db = new PouchDB( prefix + POUCH_SEPARATOR + local_db_id, local_pouch_options diff --git a/app/src/sync/process-initialization.ts b/app/src/sync/process-initialization.ts index 5767afa0f..0b4925887 100644 --- a/app/src/sync/process-initialization.ts +++ b/app/src/sync/process-initialization.ts @@ -120,8 +120,6 @@ export function reprocess_listing(listing_id: string) { // so it's an error if it doesn't exist. err => events.emit('listing_error', listing_id, err) ); - // FIXME: This is a workaround until we add notebook-level activation - window.location.reload(); } /** diff --git a/app/src/utils/helpers.tsx b/app/src/utils/helpers.tsx index 18cb8db45..b2494dc99 100644 --- a/app/src/utils/helpers.tsx +++ b/app/src/utils/helpers.tsx @@ -1,10 +1,10 @@ import {TokenContents} from '@faims3/data-model'; -export function tokenExists(token: null | undefined | TokenContents) { +function tokenExists(token: null | undefined | TokenContents) { return token !== null && token !== undefined; } -export function tokenValid(token: null | undefined | TokenContents) { +function tokenValid(token: null | undefined | TokenContents) { /** * Check for expiry AND validity */ From d6939b8eacb5072bff35691dbbb0eadb45302174 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 09:21:50 +1000 Subject: [PATCH 07/34] Fix lint warnings Signed-off-by: Steve Cassidy --- api/src/auth_routes.ts | 12 +++++++---- api/src/core.ts | 20 +++++++++---------- .../components/authentication/auth_return.tsx | 4 ++-- .../components/authentication/login_form.tsx | 7 +------ app/src/gui/themes/default/appBar.tsx | 1 - app/src/users.ts | 2 +- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/api/src/auth_routes.ts b/api/src/auth_routes.ts index fd615fcf9..bd7a7a019 100644 --- a/api/src/auth_routes.ts +++ b/api/src/auth_routes.ts @@ -119,7 +119,7 @@ export function add_auth_routes(app: any, handlers: any) { next: any, redirect: string ) => { - return (err: any, user: any, info: any) => { + return (err: any, user: any) => { if (err) { return next(err); } @@ -136,7 +136,11 @@ export function add_auth_routes(app: any, handlers: any) { }; }; - const redirect_with_token = async (res: any, user: Express.User, redirect: string) => { + const redirect_with_token = async ( + res: any, + user: Express.User, + redirect: string + ) => { // Generate a token const token = await generateUserToken(user); // Append the token to the redirect URL @@ -144,8 +148,7 @@ export function add_auth_routes(app: any, handlers: any) { // Redirect to the app with the token return res.redirect(redirectUrlWithToken); - } - + }; app.post('/auth/local', (req: any, res: any, next: any) => { const redirect = validateRedirect(req.query?.redirect || '/'); @@ -268,6 +271,7 @@ export function add_auth_routes(app: any, handlers: any) { authenticate_return(req, res, next, redirect) // HANDLER_OPTIONS[handler] )(req, res, next); + console.log('TODO: may need to insert:', HANDLER_OPTIONS[handler]); } else { throw Error( `state must be a string, or not set, not ${typeof req.query?.state}` diff --git a/api/src/core.ts b/api/src/core.ts index e143f488f..dde882959 100644 --- a/api/src/core.ts +++ b/api/src/core.ts @@ -53,16 +53,16 @@ import markdownit from 'markdown-it'; export const app = express(); app.use(morgan('combined')); -// if (process.env.NODE_ENV !== 'test') { -// // set up rate limiter: maximum of 30 requests per minute -// const limiter = RateLimit({ -// windowMs: 1 * 60 * 1000, // 1 minute -// max: 30, -// validate: true, -// }); -// app.use(limiter); -// console.log('Rate limiter enabled'); -// } +if (process.env.NODE_ENV !== 'test') { + // set up rate limiter: maximum of 30 requests per minute + const limiter = RateLimit({ + windowMs: 1 * 60 * 1000, // 1 minute + max: 30, + validate: true, + }); + app.use(limiter); + console.log('Rate limiter enabled'); +} // Only parse query parameters into strings, not objects app.set('query parser', 'simple'); diff --git a/app/src/gui/components/authentication/auth_return.tsx b/app/src/gui/components/authentication/auth_return.tsx index fad8fe7da..7a3468f65 100644 --- a/app/src/gui/components/authentication/auth_return.tsx +++ b/app/src/gui/components/authentication/auth_return.tsx @@ -24,7 +24,7 @@ import {decodeJwt} from 'jose'; import {useNavigate} from 'react-router'; import {setTokenForCluster} from '../../../users'; import {getSyncableListingsInfo} from '../../../databaseAccess'; -import {useEffect} from 'react'; +import {Dispatch, SetStateAction, useEffect} from 'react'; import {TokenContents} from '@faims3/data-model'; import {reprocess_listing} from '../../../sync/process-initialization'; @@ -41,7 +41,7 @@ async function getListingForConductorUrl(conductor_url: string) { } interface AuthReturnProps { - setToken: (token: TokenContents) => {}; + setToken: Dispatch>; } export function AuthReturn(props: AuthReturnProps) { diff --git a/app/src/gui/components/authentication/login_form.tsx b/app/src/gui/components/authentication/login_form.tsx index a3b31f93d..bcf1bd959 100644 --- a/app/src/gui/components/authentication/login_form.tsx +++ b/app/src/gui/components/authentication/login_form.tsx @@ -44,12 +44,7 @@ export function LoginButton(props: LoginButtonProps) { window.addEventListener( 'message', async event => { - await setTokenForCluster( - event.data.token, - event.data.pubkey, - event.data.pubalg, - props.listing_id - ) + await setTokenForCluster(event.data.token, props.listing_id) .then(async () => { const token = await getTokenContentsForCluster( props.listing_id diff --git a/app/src/gui/themes/default/appBar.tsx b/app/src/gui/themes/default/appBar.tsx index 6a0f617e8..5712414ae 100644 --- a/app/src/gui/themes/default/appBar.tsx +++ b/app/src/gui/themes/default/appBar.tsx @@ -1,6 +1,5 @@ import {createTheme} from '@mui/material'; import {createUseStyles as makeStyles} from 'react-jss'; -import {NavLink} from 'react-router-dom'; const theme = createTheme(); diff --git a/app/src/users.ts b/app/src/users.ts index 1b05f65a6..8b466c0b4 100644 --- a/app/src/users.ts +++ b/app/src/users.ts @@ -21,7 +21,7 @@ * on the happy path of not seeing access denied, or at least in ways the GUI * can meaningfully handle. */ -import {jwtVerify, KeyLike, importSPKI, decodeJwt} from 'jose'; +import {decodeJwt} from 'jose'; import {CLUSTER_ADMIN_GROUP_NAME} from './buildconfig'; import {LocalAuthDoc, JWTTokenMap, local_auth_db} from './sync/databases'; From b39b75af0edde56b8d3f2cc9cd84f19e5749082b Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 09:35:09 +1000 Subject: [PATCH 08/34] fix args to setTokenForCluster Signed-off-by: Steve Cassidy --- app/src/native_hooks.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/native_hooks.ts b/app/src/native_hooks.ts index b6762b7d1..e1d15337a 100644 --- a/app/src/native_hooks.ts +++ b/app/src/native_hooks.ts @@ -63,12 +63,7 @@ function processUrlPassedToken(token_obj: TokenURLObject) { getListingForConductorUrl(token_obj.origin) .then(async listing_id => { console.log('Received token via url for:', listing_id); - await setTokenForCluster( - token_obj.token, - token_obj.pubkey, - token_obj.pubalg, - listing_id - ); + await setTokenForCluster(token_obj.token, listing_id); return listing_id; }) .then(async listing_id => { From 222583c3794231fda75ca201496ebbe70fa2f446 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 10:40:53 +1000 Subject: [PATCH 09/34] Fix up app unit tests Signed-off-by: Steve Cassidy --- app/src/gui/components/notebook/refresh.tsx | 2 ++ app/src/gui/pages/about-build.test.tsx | 2 +- app/src/gui/pages/workspace.test.tsx | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/gui/components/notebook/refresh.tsx b/app/src/gui/components/notebook/refresh.tsx index 1f98670cc..d653a7bec 100644 --- a/app/src/gui/components/notebook/refresh.tsx +++ b/app/src/gui/components/notebook/refresh.tsx @@ -87,6 +87,7 @@ export default function RefreshNotebook(props: RefreshNotebookProps) { return ( } onClick={handleRefresh} variant="contained" + data-testid="refreshRecords" sx={{marginLeft: '12px', padding: '4px 8px', fontSize: '0.75rem'}} > Refresh diff --git a/app/src/gui/pages/about-build.test.tsx b/app/src/gui/pages/about-build.test.tsx index 38fd340e4..d1b4465cc 100644 --- a/app/src/gui/pages/about-build.test.tsx +++ b/app/src/gui/pages/about-build.test.tsx @@ -35,7 +35,7 @@ test('Check about-build component', async () => { ); - expect(screen.getByText('Directory Server:')).toBeTruthy(); + expect(screen.getByText('Servers:')).toBeTruthy(); expect(screen.getByText('Refresh the app')).toBeTruthy(); diff --git a/app/src/gui/pages/workspace.test.tsx b/app/src/gui/pages/workspace.test.tsx index cb3751131..1b7ce6c41 100644 --- a/app/src/gui/pages/workspace.test.tsx +++ b/app/src/gui/pages/workspace.test.tsx @@ -22,7 +22,7 @@ import {act, render, screen} from '@testing-library/react'; import {BrowserRouter as Router} from 'react-router-dom'; import Workspace from './workspace'; import {test, expect} from 'vitest'; -import {NOTEBOOK_NAME_CAPITALIZED} from '../../buildconfig'; +import {NOTEBOOK_NAME} from '../../buildconfig'; test('Check workspace component', async () => { act(() => { @@ -32,5 +32,5 @@ test('Check workspace component', async () => { ); }); - expect(screen.getByText(`My ${NOTEBOOK_NAME_CAPITALIZED}s`)).toBeTruthy(); + expect(screen.getByText(`Loading ${NOTEBOOK_NAME}s`)).toBeTruthy(); }); From bea8e935748d3b1be328105f1762c56667c84b3c Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 11:12:11 +1000 Subject: [PATCH 10/34] fix nesting warning in description page Signed-off-by: Steve Cassidy --- app/src/gui/components/notebook/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/gui/components/notebook/index.tsx b/app/src/gui/components/notebook/index.tsx index b911ed704..ae7947236 100644 --- a/app/src/gui/components/notebook/index.tsx +++ b/app/src/gui/components/notebook/index.tsx @@ -334,6 +334,7 @@ export default function NotebookComponent(props: NotebookComponentProps) { Description:{' '} From 4ab494c3d11cbc0ad9b79c2b5e79d63df3cdab40 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 11:12:39 +1000 Subject: [PATCH 11/34] Remove some debug output Signed-off-by: Steve Cassidy --- app/src/sync/databases.ts | 5 ----- app/src/sync/events.ts | 17 ++++++++--------- app/src/sync/state.ts | 6 ------ app/src/users.ts | 1 - app/src/utils/helpers.tsx | 1 - 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/app/src/sync/databases.ts b/app/src/sync/databases.ts index f415770ec..948f36425 100644 --- a/app/src/sync/databases.ts +++ b/app/src/sync/databases.ts @@ -296,11 +296,6 @@ export function setLocalConnection( db_info: LocalDB & {remote: LocalDBRemote} ) { const options = db_info.remote.options; - console.debug( - '%cSetting local connection:', - 'background-color: cyan;', - db_info - ); if (db_info.is_sync) { if (db_info.remote.connection !== null) { diff --git a/app/src/sync/events.ts b/app/src/sync/events.ts index 0a0945f21..f189513ff 100644 --- a/app/src/sync/events.ts +++ b/app/src/sync/events.ts @@ -22,7 +22,6 @@ import {EventEmitter} from 'events'; -import {DEBUG_APP} from '../buildconfig'; import {ListingID} from '@faims3/data-model'; import {ProjectObject} from './projects'; import {ListingsObject, ExistingActiveDoc} from './databases'; @@ -34,14 +33,14 @@ export class DebugEmitter extends EventEmitter { super(opts); } emit(event: string | symbol, ...args: unknown[]): boolean { - if (DEBUG_APP) { - console.log( - '%cFAIMS EventEmitter event', - 'background-color: red; color: white;', - event, - ...args - ); - } + // if (DEBUG_APP) { + // console.log( + // '%cFAIMS EventEmitter event', + // 'background-color: red; color: white;', + // event, + // ...args + // ); + // } return super.emit(event, ...args); } } diff --git a/app/src/sync/state.ts b/app/src/sync/state.ts index 5526d54b1..6352f2917 100644 --- a/app/src/sync/state.ts +++ b/app/src/sync/state.ts @@ -157,12 +157,6 @@ export function register_sync_state(initializeEvents: DirectoryEmitter) { all_projects_updated && Array.from(projects_data_synced.values()).every(v => v); - console.log( - 'COMMON CHECK', - all_projects_updated, - !listings_updated, - listing_projects_synced - ); initializeEvents.emit('all_state'); }; diff --git a/app/src/users.ts b/app/src/users.ts index 8b466c0b4..5e3cad07e 100644 --- a/app/src/users.ts +++ b/app/src/users.ts @@ -395,7 +395,6 @@ export async function getTokenContentsForCurrentUser(): Promise< TokenContents | undefined > { const docs = await local_auth_db.allDocs(); - console.log('GOT DOCS', docs); if (docs.total_rows > 0) { const cluster_id = docs.rows[0].id; return getTokenContentsForCluster(cluster_id); diff --git a/app/src/utils/helpers.tsx b/app/src/utils/helpers.tsx index b2494dc99..ab5e113af 100644 --- a/app/src/utils/helpers.tsx +++ b/app/src/utils/helpers.tsx @@ -19,6 +19,5 @@ export function checkToken(token: null | undefined | TokenContents) { /** * Check if the token exists, and whether it's valid */ - console.log('checkToken', token); return tokenExists(token) && tokenValid(token); } From 762d3d8be687699a40fbd26cdaf0e05450dc53f8 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 14:41:41 +1000 Subject: [PATCH 12/34] login flow working on android Signed-off-by: Steve Cassidy --- api/src/auth_routes.ts | 5 +- .../app/src/main/assets/capacitor.config.json | 6 +- app/capacitor.config.json | 6 +- app/ios/App/App/capacitor.config.json | 6 +- app/src/App.tsx | 2 + .../components/authentication/auth_return.tsx | 4 +- .../components/authentication/login_form.tsx | 6 +- app/src/native_hooks.ts | 78 ++++--------------- 8 files changed, 45 insertions(+), 68 deletions(-) diff --git a/api/src/auth_routes.ts b/api/src/auth_routes.ts index bd7a7a019..bf5968535 100644 --- a/api/src/auth_routes.ts +++ b/api/src/auth_routes.ts @@ -75,7 +75,10 @@ function validateRedirect(redirect: string) { if (redirect.startsWith('http')) { // should match against a whitelist of allowed URLs return redirect; - } else if (redirect.startsWith('/')) { + } else if ( + redirect.startsWith('/') || + redirect.startsWith('org.fedarch.faims3://') + ) { return redirect; } else { return '/'; diff --git a/app/android/app/src/main/assets/capacitor.config.json b/app/android/app/src/main/assets/capacitor.config.json index 2899e3cbe..91a9e3d29 100644 --- a/app/android/app/src/main/assets/capacitor.config.json +++ b/app/android/app/src/main/assets/capacitor.config.json @@ -6,10 +6,14 @@ "plugins": { "SplashScreen": { "launchShowDuration": 0 + }, + "CapacitorHttp": { + "enabled": false } }, "server": { - "allowNavigation": [] + "allowNavigation": [], + "iosScheme": "org.fedarch.faims3" }, "android": { "webContentsDebuggingEnabled": true diff --git a/app/capacitor.config.json b/app/capacitor.config.json index 3e78960d8..efe74dc73 100644 --- a/app/capacitor.config.json +++ b/app/capacitor.config.json @@ -6,10 +6,14 @@ "plugins": { "SplashScreen": { "launchShowDuration": 0 + }, + "CapacitorHttp": { + "enabled": false } }, "server": { - "allowNavigation": [] + "allowNavigation": [], + "iosScheme": "org.fedarch.faims3" }, "android": { "webContentsDebuggingEnabled": true diff --git a/app/ios/App/App/capacitor.config.json b/app/ios/App/App/capacitor.config.json index 5c326e838..7110cbee8 100644 --- a/app/ios/App/App/capacitor.config.json +++ b/app/ios/App/App/capacitor.config.json @@ -6,10 +6,14 @@ "plugins": { "SplashScreen": { "launchShowDuration": 0 + }, + "CapacitorHttp": { + "enabled": true } }, "server": { - "allowNavigation": [] + "allowNavigation": [], + "iosScheme": "fieldmark" }, "android": { "webContentsDebuggingEnabled": true diff --git a/app/src/App.tsx b/app/src/App.tsx index 79fdb0885..b3b2f4b3d 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -45,6 +45,7 @@ import {useEffect, useState} from 'react'; import {TokenContents} from '@faims3/data-model'; import NotFound404 from './gui/pages/404'; import {AuthReturn} from './gui/components/authentication/auth_return'; +import {AppUrlListener} from './native_hooks'; // type AppProps = {}; @@ -73,6 +74,7 @@ export default function App() { + Auth Token; } + diff --git a/app/src/gui/components/authentication/login_form.tsx b/app/src/gui/components/authentication/login_form.tsx index bcf1bd959..c866c1377 100644 --- a/app/src/gui/components/authentication/login_form.tsx +++ b/app/src/gui/components/authentication/login_form.tsx @@ -72,7 +72,11 @@ export function LoginButton(props: LoginButtonProps) { } } else { // Use the capacitor browser plugin in apps - await Browser.open({url: props.conductor_url}); + await Browser.open({ + url: + props.conductor_url + + '/auth?redirect=org.fedarch.faims3://auth-return', + }); } }} > diff --git a/app/src/native_hooks.ts b/app/src/native_hooks.ts index e1d15337a..b3684a0cc 100644 --- a/app/src/native_hooks.ts +++ b/app/src/native_hooks.ts @@ -19,17 +19,24 @@ * native parts of the system. */ -import {App as CapacitorApp} from '@capacitor/app'; - -import {getSyncableListingsInfo} from './databaseAccess'; -import {setTokenForCluster} from './users'; -import {reprocess_listing} from './sync/process-initialization'; +import {App as CapacitorApp, URLOpenListenerEvent} from '@capacitor/app'; +import React, {useEffect} from 'react'; +import {useNavigate} from 'react-router-dom'; + +export function AppUrlListener() { + const navigate = useNavigate(); + + useEffect(() => { + CapacitorApp.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { + const url = new URL(event.url); + // remove the first '/' from the pathname + const redirect = url.pathname.substring(1) + url.search; + console.log('navigating from app url to', redirect); + navigate(redirect); + }); + }, []); -interface TokenURLObject { - token: string; - pubkey: string; - pubalg: string; - origin: string; + return null; } export function addNativeHooks() { @@ -37,58 +44,7 @@ export function addNativeHooks() { console.log('App state changed. Is active?', isActive); }); - CapacitorApp.addListener('appUrlOpen', data => { - console.log('App opened with URL:', data); - parseAndHandleAppUrl(data.url); - }); - CapacitorApp.addListener('appRestoredResult', data => { console.log('Restored state:', data); }); } - -async function getListingForConductorUrl(conductor_url: string) { - const origin = new URL(conductor_url).origin; - const listings = await getSyncableListingsInfo(); - for (const l of listings) { - const possible_origin = new URL(l.conductor_url).origin; - if (possible_origin === origin) { - return l.id; - } - } - throw Error(`Unknown listing for conductor url ${conductor_url}`); -} - -function processUrlPassedToken(token_obj: TokenURLObject) { - getListingForConductorUrl(token_obj.origin) - .then(async listing_id => { - console.log('Received token via url for:', listing_id); - await setTokenForCluster(token_obj.token, listing_id); - return listing_id; - }) - .then(async listing_id => { - reprocess_listing(listing_id); - }) - .catch(err => { - console.warn('Failed to get token from url', err); - }); -} - -function parseAndHandleAppUrl(url_s: string) { - const url = new URL(url_s); - if (url.hostname === 'auth') { - // Drop / from pathname - const urlenc_token = url.pathname.substring(1); - const token = JSON.parse(decodeURIComponent(urlenc_token)); - console.debug('Parsed url token', token); - processUrlPassedToken(token as TokenURLObject); - } else if (url.pathname.startsWith('//auth/')) { - // Drop //auth/ from pathname - const urlenc_token = url.pathname.substring(7); - const token = JSON.parse(decodeURIComponent(urlenc_token)); - console.debug('Parsed url token', token); - processUrlPassedToken(token as TokenURLObject); - } else { - console.warn('App url not handled', url_s, url); - } -} From 9bb86533e0385ed913042447fcc4ab18a3f1fc64 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Fri, 6 Sep 2024 16:27:04 +1000 Subject: [PATCH 13/34] make registration route honour redirects Signed-off-by: Steve Cassidy --- api/src/auth_routes.ts | 26 ++++++++++++++++++++------ api/views/register.handlebars | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/api/src/auth_routes.ts b/api/src/auth_routes.ts index bf5968535..5b5b33bfb 100644 --- a/api/src/auth_routes.ts +++ b/api/src/auth_routes.ts @@ -164,6 +164,7 @@ export function add_auth_routes(app: any, handlers: any) { // accept an invite, auth not required, we invite them to // register if they aren't already app.get('/register/:invite_id/', async (req: any, res: any) => { + const redirect = validateRedirect(req.query?.redirect || '/'); const invite_id = req.params.invite_id; req.session['invite'] = invite_id; const invite = await getInvite(invite_id); @@ -179,7 +180,7 @@ export function add_auth_routes(app: any, handlers: any) { 'message', 'You will now have access to the ${invite.notebook} notebook.' ); - res.redirect('/'); + redirect_with_token(res, req.user, redirect); } else { // need to sign up the user, show the registration page const available_provider_info = []; @@ -192,6 +193,7 @@ export function add_auth_routes(app: any, handlers: any) { res.render('register', { invite: invite_id, providers: available_provider_info, + redirect: redirect, localAuth: true, // maybe make this configurable? messages: req.flash(), }); @@ -205,13 +207,14 @@ export function add_auth_routes(app: any, handlers: any) { .isLength({min: 10}) .withMessage('Must be at least 10 characters'), body('email').isEmail().withMessage('Must be a valid email address'), - async (req: any, res: any) => { + async (req: any, res: any, next: any) => { // create a new local account if we have a valid invite const username = req.body.username; const password = req.body.password; const repeat = req.body.repeat; const name = req.body.name; const email = req.body.email; + const redirect = validateRedirect(req.body.redirect || '/'); const errors = validationResult(req); @@ -221,7 +224,9 @@ export function add_auth_routes(app: any, handlers: any) { req.flash('email', email); req.flash('name', name); res.status(400); - res.redirect('/register/' + req.session.invite); + res.redirect( + '/register/' + req.session.invite + `?redirect=${redirect}` + ); return; } @@ -241,14 +246,21 @@ export function add_auth_routes(app: any, handlers: any) { if (user) { await acceptInvite(user, invite); req.flash('message', 'Registration successful. Please login below.'); - res.redirect('/'); + req.login(user, (err: any) => { + if (err) { + return next(err); + } + return redirect_with_token(res, user, redirect); + }); } else { req.flash('error', {registration: error}); req.flash('username', username); req.flash('email', email); req.flash('name', name); res.status(400); - res.redirect('/register/' + req.session.invite); + res.redirect( + '/register/' + req.session.invite + `?redirect=${redirect}` + ); } } else { req.flash('error', {repeat: {msg: "Password and repeat don't match."}}); @@ -256,7 +268,9 @@ export function add_auth_routes(app: any, handlers: any) { req.flash('email', email); req.flash('name', name); res.status(400); - res.redirect('/register/' + req.session.invite); + res.redirect( + '/register/' + req.session.invite + `?redirect=${redirect}` + ); } } ); diff --git a/api/views/register.handlebars b/api/views/register.handlebars index 353c7e7b4..1f20dd76c 100644 --- a/api/views/register.handlebars +++ b/api/views/register.handlebars @@ -16,6 +16,7 @@
{{messages.error.registration}}
{{/if}} +
Register Local Account
From d024402de642fa5176445a6690dddeb011f95232 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Mon, 9 Sep 2024 07:44:21 +1000 Subject: [PATCH 14/34] Updates for IOS build Signed-off-by: Steve Cassidy --- app/ios/App/App.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/WorkspaceSettings.xcsettings | 5 +++++ app/ios/App/App/Info.plist | 2 +- app/ios/App/App/capacitor.config.json | 4 ++-- app/src/gui/components/authentication/auth_return.tsx | 2 +- app/src/native_hooks.ts | 2 ++ 6 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 app/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/app/ios/App/App.xcodeproj/project.pbxproj b/app/ios/App/App.xcodeproj/project.pbxproj index b37fbc058..fb2ff341d 100644 --- a/app/ios/App/App.xcodeproj/project.pbxproj +++ b/app/ios/App/App.xcodeproj/project.pbxproj @@ -117,7 +117,6 @@ FA46845DBB8EA1E35A493838 /* Pods-App.debug.xcconfig */, 12EF5F1187B9DEB05CE134E3 /* Pods-App.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -438,6 +437,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; + ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = au.edu.faims.electronicfieldnotebook; PRODUCT_NAME = "${TARGET_NAME}"; diff --git a/app/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/app/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/app/ios/App/App/Info.plist b/app/ios/App/App/Info.plist index 185b49c72..053b09a30 100644 --- a/app/ios/App/App/Info.plist +++ b/app/ios/App/App/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 202407170719 + 202409082135 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/app/ios/App/App/capacitor.config.json b/app/ios/App/App/capacitor.config.json index 7110cbee8..cb9c31e33 100644 --- a/app/ios/App/App/capacitor.config.json +++ b/app/ios/App/App/capacitor.config.json @@ -8,12 +8,12 @@ "launchShowDuration": 0 }, "CapacitorHttp": { - "enabled": true + "enabled": false } }, "server": { "allowNavigation": [], - "iosScheme": "fieldmark" + "iosScheme": "org.fedarch.faims3" }, "android": { "webContentsDebuggingEnabled": true diff --git a/app/src/gui/components/authentication/auth_return.tsx b/app/src/gui/components/authentication/auth_return.tsx index 116da0929..b13d4a989 100644 --- a/app/src/gui/components/authentication/auth_return.tsx +++ b/app/src/gui/components/authentication/auth_return.tsx @@ -21,7 +21,7 @@ */ import {decodeJwt} from 'jose'; -import {NavigateFunction, useNavigate} from 'react-router'; +import {useNavigate} from 'react-router'; import {setTokenForCluster} from '../../../users'; import {getSyncableListingsInfo} from '../../../databaseAccess'; import {Dispatch, SetStateAction, useEffect} from 'react'; diff --git a/app/src/native_hooks.ts b/app/src/native_hooks.ts index b3684a0cc..c53d12ac2 100644 --- a/app/src/native_hooks.ts +++ b/app/src/native_hooks.ts @@ -20,6 +20,7 @@ */ import {App as CapacitorApp, URLOpenListenerEvent} from '@capacitor/app'; +import {Browser} from '@capacitor/browser'; import React, {useEffect} from 'react'; import {useNavigate} from 'react-router-dom'; @@ -31,6 +32,7 @@ export function AppUrlListener() { const url = new URL(event.url); // remove the first '/' from the pathname const redirect = url.pathname.substring(1) + url.search; + Browser.close(); console.log('navigating from app url to', redirect); navigate(redirect); }); From 3e22440d3c02ae09af3b13f334038f375a7e4158 Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Wed, 11 Sep 2024 10:51:21 +1000 Subject: [PATCH 15/34] Working login on IOS + Android Signed-off-by: Steve Cassidy --- app/ios/App/App.xcodeproj/project.pbxproj | 7 ++----- app/ios/App/App/Info.plist | 2 +- app/src/gui/pages/about-build.test.tsx | 2 +- app/src/native_hooks.ts | 6 ++++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/ios/App/App.xcodeproj/project.pbxproj b/app/ios/App/App.xcodeproj/project.pbxproj index fb2ff341d..01043c8b9 100644 --- a/app/ios/App/App.xcodeproj/project.pbxproj +++ b/app/ios/App/App.xcodeproj/project.pbxproj @@ -422,11 +422,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "Auto-set by script in build phases"; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = WTH5X6HHX7; + DEVELOPMENT_TEAM = WTH5X6HHX7; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = App/Info.plist; INFOPLIST_PREFIX_HEADER = Plist/Prefix; @@ -442,7 +440,6 @@ PRODUCT_BUNDLE_IDENTIFIER = au.edu.faims.electronicfieldnotebook; PRODUCT_NAME = "${TARGET_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore au.edu.faims.electronicfieldnotebook"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/app/ios/App/App/Info.plist b/app/ios/App/App/Info.plist index 053b09a30..c4bc99d88 100644 --- a/app/ios/App/App/Info.plist +++ b/app/ios/App/App/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 202409082135 + 202409110011 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/app/src/gui/pages/about-build.test.tsx b/app/src/gui/pages/about-build.test.tsx index d1b4465cc..2718b5770 100644 --- a/app/src/gui/pages/about-build.test.tsx +++ b/app/src/gui/pages/about-build.test.tsx @@ -35,7 +35,7 @@ test('Check about-build component', async () => { ); - expect(screen.getByText('Servers:')).toBeTruthy(); + expect(screen.getByText('Server:')).toBeTruthy(); expect(screen.getByText('Refresh the app')).toBeTruthy(); diff --git a/app/src/native_hooks.ts b/app/src/native_hooks.ts index c53d12ac2..fa982d5c9 100644 --- a/app/src/native_hooks.ts +++ b/app/src/native_hooks.ts @@ -30,8 +30,10 @@ export function AppUrlListener() { useEffect(() => { CapacitorApp.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { const url = new URL(event.url); - // remove the first '/' from the pathname - const redirect = url.pathname.substring(1) + url.search; + console.log('Event URL', url); + // grab the 'pathname' part of the URL, note that url.pathname + // is not correct on Safari so we go the long way around + const redirect = url.href.substring(url.protocol.length + 1); Browser.close(); console.log('navigating from app url to', redirect); navigate(redirect); From 27d64ce8b520bd921f876b6df3a936323b88a68f Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Wed, 11 Sep 2024 12:16:11 +1000 Subject: [PATCH 16/34] fix app initialisation after login Signed-off-by: Steve Cassidy --- .../components/authentication/auth_return.tsx | 54 ++++++++++--------- app/src/sync/process-initialization.ts | 2 + app/src/users.ts | 2 +- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/app/src/gui/components/authentication/auth_return.tsx b/app/src/gui/components/authentication/auth_return.tsx index b13d4a989..66ba08971 100644 --- a/app/src/gui/components/authentication/auth_return.tsx +++ b/app/src/gui/components/authentication/auth_return.tsx @@ -26,7 +26,7 @@ import {setTokenForCluster} from '../../../users'; import {getSyncableListingsInfo} from '../../../databaseAccess'; import {Dispatch, SetStateAction, useEffect} from 'react'; import {TokenContents} from '@faims3/data-model'; -import {reprocess_listing} from '../../../sync/process-initialization'; +import {update_directory} from '../../../sync/process-initialization'; async function getListingForConductorUrl(conductor_url: string) { const origin = new URL(conductor_url).origin; @@ -48,33 +48,38 @@ export function AuthReturn(props: AuthReturnProps) { const navigate = useNavigate(); useEffect(() => { + const storeToken = async (token: string) => { + const token_obj = decodeJwt(decodeURIComponent(token)); + + console.log('decoded', token_obj); + + const listing_id = await getListingForConductorUrl( + token_obj.server as string + ); + + console.log('Received token via url for:', listing_id); + await setTokenForCluster(token, listing_id); + console.log('We have stored the token for ', listing_id); + // this requires the token + update_directory(); + // generate the TokenContents object like parseToken does + const token_content = { + username: token_obj.sub as string, + roles: (token_obj['_couchdb.roles'] as string[]) || '', + name: token_obj.name as string, + }; + console.log('%cToken content', 'background-color: green', token_content); + props.setToken(token_content); + navigate('/'); + }; + const params = new URLSearchParams(window.location.search); if (params.has('token')) { const token = params.get('token'); if (token) { - const token_obj = decodeJwt(decodeURIComponent(token)); - - console.log('decoded', token_obj); - getListingForConductorUrl(token_obj.server as string) - .then(async listing_id => { - console.log('Received token via url for:', listing_id); - setTokenForCluster(token, listing_id).then(() => { - console.log('We have stored the token for ', listing_id); - reprocess_listing(listing_id); - // generate the TokenContents object like parseToken does - const token_content = { - username: token_obj.sub as string, - roles: (token_obj['_couchdb.roles'] as string[]) || '', - name: token_obj.name as string, - }; - console.log('%ctoken content', 'color: red', token_content); - props.setToken(token_content); - navigate('/'); - }); - }) - .catch(err => { - console.warn('Failed to get token from url', err); - }); + storeToken(token).catch(err => { + console.error(err); + }); } } else { navigate('/'); @@ -83,4 +88,3 @@ export function AuthReturn(props: AuthReturnProps) { return

Auth Token

; } - diff --git a/app/src/sync/process-initialization.ts b/app/src/sync/process-initialization.ts index 0b4925887..fd252e501 100644 --- a/app/src/sync/process-initialization.ts +++ b/app/src/sync/process-initialization.ts @@ -50,6 +50,7 @@ import {ensure_project_databases} from './projects'; * Called on startup or page/app refresh. */ export async function update_directory() { + console.log('UPDATE DIRECTORY'); // get existing stored listings from the local db const listings = await getAllListings(); @@ -198,6 +199,7 @@ async function get_projects_from_conductor(listing: ListingsObject) { // get the remote data const jwt_token = await getTokenForCluster(listing._id); + console.log('%cDid we get a token:', 'background-color: red', jwt_token); // if we have no token, then don't try to fetch if (!jwt_token) return; diff --git a/app/src/users.ts b/app/src/users.ts index 5e3cad07e..b9b665ac0 100644 --- a/app/src/users.ts +++ b/app/src/users.ts @@ -88,7 +88,7 @@ export async function setTokenForCluster(token: string, cluster_id: string) { throw Error(`Failed to set token when conflicting for: ${cluster_id}`); } } catch (err) { - console.debug('Failed to get token when setting for', cluster_id, err); + console.debug('No existing token for', cluster_id); try { const doc = await addTokenToDoc(token, cluster_id, null); console.debug('Initial token info is:', doc); From 3cd84ee58f82521ab1941869556a6b6660014b1e Mon Sep 17 00:00:00 2001 From: Steve Cassidy Date: Wed, 11 Sep 2024 17:00:10 +1000 Subject: [PATCH 17/34] Invites have six character short codes, only one per notebook/role Signed-off-by: Steve Cassidy --- api/src/couchdb/invites.ts | 74 +++++++++++++++++++++------ api/src/datamodel/users.ts | 3 -- api/src/registration.ts | 10 ---- api/src/routes.ts | 4 +- api/test/invites.test.ts | 15 ++---- api/views/invite.handlebars | 12 ----- api/views/my-invites.handlebars | 51 ------------------ api/views/notebook-landing.handlebars | 17 +++--- 8 files changed, 74 insertions(+), 112 deletions(-) delete mode 100644 api/views/my-invites.handlebars diff --git a/api/src/couchdb/invites.ts b/api/src/couchdb/invites.ts index 47e0b580e..c1cce3d99 100644 --- a/api/src/couchdb/invites.ts +++ b/api/src/couchdb/invites.ts @@ -21,30 +21,74 @@ import {NonUniqueProjectID, ProjectID} from '@faims3/data-model'; import {getInvitesDB} from '.'; import {ConductorRole, RoleInvite} from '../datamodel/users'; -import {v4 as uuidv4} from 'uuid'; +/** + * Create an invite for this project and role if there isn't already + * one. If it already exists, return it. + * @param project_id Project identifier + * @param role Project role + * @returns A RoleInvite object + */ export async function createInvite( - user: Express.User, project_id: NonUniqueProjectID, - role: ConductorRole, - number: number + role: ConductorRole ) { - const invite: RoleInvite = { - _id: uuidv4(), - requesting_user: user.user_id, - project_id: project_id, - role: role, - number: number, - unlimited: number === 0, - }; - await saveInvite(invite); - return invite; + // if there is already an invite for this role, + // just return that + const allInvites = await getInvitesForNotebook(project_id); + const existing = allInvites.filter( + i => i.project_id === project_id && i.role === role + ); + + if (existing.length === 0) { + // make a new one + const invite: RoleInvite = { + _id: generateId(), + project_id: project_id, + role: role, + }; + return await saveInvite(invite); + } else { + return existing[0]; + } } +/** + * Generate a short code identifier suitable for an invite, may not + * be unique. + * @returns a six character identifier + */ +function generateId() { + const INVITE_LENGTH = 6; + const chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'; + + let ident = ''; + for (let i = 0; i < INVITE_LENGTH; i++) { + const char = chars[Math.floor(Math.random() * chars.length)]; + ident = ident + char; + } + return ident; +} + +/** + * Store an invite, ensure that the identifier is unique + * @param invite An invite object + * @returns The invite, possibly with a new identifier + */ export async function saveInvite(invite: RoleInvite) { const invite_db = getInvitesDB(); if (invite_db) { - await invite_db.put(invite); + let done = false; + while (!done) { + try { + await invite_db.put(invite); + done = true; + console.log(invite); + } catch { + invite._id = generateId(); + } + } + return invite; } else { throw Error('Unable to connect to invites database'); } diff --git a/api/src/datamodel/users.ts b/api/src/datamodel/users.ts index 6b86001b2..de69de48e 100644 --- a/api/src/datamodel/users.ts +++ b/api/src/datamodel/users.ts @@ -66,9 +66,6 @@ export interface RoleInvite { _id: string; _rev?: string; _deleted?: boolean; - requesting_user: string; - unlimited?: boolean; project_id: NonUniqueProjectID; role: ConductorRole; - number: number; } diff --git a/api/src/registration.ts b/api/src/registration.ts index 245e8133b..043cf75e7 100644 --- a/api/src/registration.ts +++ b/api/src/registration.ts @@ -20,7 +20,6 @@ import {RoleInvite, ConductorRole} from './datamodel/users'; import {addProjectRoleToUser, saveUser} from './couchdb/users'; -import {saveInvite, deleteInvite} from './couchdb/invites'; import {CLUSTER_ADMIN_GROUP_NAME} from './buildconfig'; export function userCanAddOtherRole(user: Express.User | undefined): boolean { @@ -52,15 +51,6 @@ export function userCanRemoveOtherRole( export async function acceptInvite(user: Express.User, invite: RoleInvite) { addProjectRoleToUser(user, invite.project_id, invite.role); await saveUser(user); - - if (!invite.unlimited) { - invite.number--; - if (invite.number === 0) { - await deleteInvite(invite); - } else { - await saveInvite(invite); - } - } } export async function rejectInvite(invite: RoleInvite) { diff --git a/api/src/routes.ts b/api/src/routes.ts index c71564b96..251421210 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -83,7 +83,6 @@ app.get('/notebooks/:id/invite/', requireAuthentication, async (req, res) => { app.post( '/notebooks/:id/invite/', requireAuthentication, - body('number').not().isEmpty(), body('role').not().isEmpty(), async (req, res) => { const errors = validationResult(req); @@ -92,7 +91,6 @@ app.post( } const project_id: NonUniqueProjectID = req.params.id; const role: string = req.body.role; - const number: number = parseInt(req.body.number); if (!userHasPermission(req.user, project_id, 'modify')) { res.render('invite-error', { @@ -105,7 +103,7 @@ app.post( ], }); } else { - await createInvite(req.user as Express.User, project_id, role, number); + await createInvite(project_id, role); res.redirect('/notebooks/' + project_id); } } diff --git a/api/test/invites.test.ts b/api/test/invites.test.ts index ed7d58ba4..90409a938 100644 --- a/api/test/invites.test.ts +++ b/api/test/invites.test.ts @@ -21,7 +21,6 @@ import {ProjectUIModel} from '@faims3/data-model'; import PouchDB from 'pouchdb'; import {createNotebook} from '../src/couchdb/notebooks'; -import {getUserFromEmailOrUsername} from '../src/couchdb/users'; import { createInvite, deleteInvite, @@ -45,20 +44,17 @@ describe('Invites', () => { beforeEach(initialiseDatabases); it('create invite', async () => { - const adminUser = await getUserFromEmailOrUsername('admin'); const project_id = await createNotebook('Test Notebook', uispec, {}); const role = 'user'; - const number = 10; - if (adminUser && project_id) { - const invite = await createInvite(adminUser, project_id, role, number); + if (project_id) { + const invite = await createInvite(project_id, role); // check that it was saved - fetch from db const fetched = await getInvite(invite._id); if (fetched) { expect(fetched.project_id).to.equal(project_id); - expect(fetched.number).to.equal(number); // get invites for notebook const invites = await getInvitesForNotebook(project_id); @@ -76,20 +72,17 @@ describe('Invites', () => { }); it('create unlimited invite', async () => { - const adminUser = await getUserFromEmailOrUsername('admin'); const project_id = await createNotebook('Test Notebook', uispec, {}); const role = 'user'; - const number = 0; - if (adminUser && project_id) { - const invite = await createInvite(adminUser, project_id, role, number); + if (project_id) { + const invite = await createInvite(project_id, role); // check that it was saved - fetch from db const fetched = await getInvite(invite._id); if (fetched) { expect(fetched.project_id).to.equal(project_id); - expect(fetched.unlimited).to.be.true; } else { assert.fail('could not retrieve newly created invite'); } diff --git a/api/views/invite.handlebars b/api/views/invite.handlebars index 2294456af..8b955c982 100644 --- a/api/views/invite.handlebars +++ b/api/views/invite.handlebars @@ -23,20 +23,8 @@ Create an invitation for users to sign up to this notebook. This will generate an invite URL and QR code that can be used to register. - -
- - -
How many users should be able to - use this invite. Set to zero (0) for an unlimited number. -
-
-