diff --git a/.env.example b/.env.example index 2f99e828..7af330d6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,9 @@ # You already have access to basic local functionality (UI, authentication, database read access). +# openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -in backend/shared/src/googleApplicationCredentials-dev.json -out secrets/googleApplicationCredentials-dev.json.enc +GOOGLE_CREDENTIALS_ENC_PWD=nP7s3274uzOG4c2t + + # Optional variables for full local functionality # For the location / distance filtering features. diff --git a/.gitignore b/.gitignore index cf43b8bb..41696098 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ email-preview *.tar.gz *.rar /favicon_color.ico +/backend/shared/src/googleApplicationCredentials-dev.json diff --git a/backend/api/src/helpers/endpoint.ts b/backend/api/src/helpers/endpoint.ts index b34cfaf0..7c1e0bfe 100644 --- a/backend/api/src/helpers/endpoint.ts +++ b/backend/api/src/helpers/endpoint.ts @@ -1,35 +1,29 @@ import * as admin from 'firebase-admin' -import { z } from 'zod' -import { Request, Response, NextFunction } from 'express' +import {z} from 'zod' +import {NextFunction, Request, Response} from 'express' -import { PrivateUser } from 'common/user' -import { APIError } from 'common/api/utils' -export { APIError } from 'common/api/utils' -import { - API, - APIPath, - APIResponseOptionalContinue, - APISchema, - ValidatedAPIParams, -} from 'common/api/schema' -import { log } from 'shared/utils' -import { getPrivateUserByKey } from 'shared/utils' +import {PrivateUser} from 'common/user' +import {APIError} from 'common/api/utils' +import {API, APIPath, APIResponseOptionalContinue, APISchema, ValidatedAPIParams,} from 'common/api/schema' +import {getPrivateUserByKey, log} from 'shared/utils' -export type Json = Record | Json[] -export type JsonHandler = ( - req: Request, - res: Response -) => Promise -export type AuthedHandler = ( - req: Request, - user: AuthedUser, - res: Response -) => Promise -export type MaybeAuthedHandler = ( - req: Request, - user: AuthedUser | undefined, - res: Response -) => Promise +export {APIError} from 'common/api/utils' + +// export type Json = Record | Json[] +// export type JsonHandler = ( +// req: Request, +// res: Response +// ) => Promise +// export type AuthedHandler = ( +// req: Request, +// user: AuthedUser, +// res: Response +// ) => Promise +// export type MaybeAuthedHandler = ( +// req: Request, +// user: AuthedUser | undefined, +// res: Response +// ) => Promise export type AuthedUser = { uid: string @@ -39,6 +33,29 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken } type KeyCredentials = { kind: 'key'; data: string } type Credentials = JwtCredentials | KeyCredentials +// export async function verifyIdToken(payload: string): Promise { +// TODO: make local dev work without firebase admin SDK setup. +// if (IS_LOCAL) { +// // Skip real verification locally (to avoid needing to set up admin service account). +// return { +// aud: "", +// auth_time: 0, +// email_verified: false, +// exp: 0, +// firebase: {identities: {}, sign_in_provider: ""}, +// iat: 0, +// iss: "", +// phone_number: "", +// picture: "", +// sub: "", +// uid: 'dev-user', +// user_id: 'dev-user', +// email: 'dev-user@example.com' +// }; +// } +// return await admin.auth().verifyIdToken(payload); +// } + export const parseCredentials = async (req: Request): Promise => { const auth = admin.auth() const authHeader = req.get('Authorization') @@ -57,14 +74,14 @@ export const parseCredentials = async (req: Request): Promise => { throw new APIError(401, 'Firebase JWT payload undefined.') } try { - return { kind: 'jwt', data: await auth.verifyIdToken(payload) } + return {kind: 'jwt', data: await auth.verifyIdToken(payload)} } catch (err) { // This is somewhat suspicious, so get it into the firebase console console.error('Error verifying Firebase JWT: ', err, scheme, payload) throw new APIError(500, 'Error validating token.') } case 'Key': - return { kind: 'key', data: payload } + return {kind: 'key', data: payload} default: throw new APIError(401, 'Invalid auth scheme; must be "Key" or "Bearer".') } @@ -76,7 +93,7 @@ export const lookupUser = async (creds: Credentials): Promise => { if (typeof creds.data.user_id !== 'string') { throw new APIError(401, 'JWT must contain user ID.') } - return { uid: creds.data.user_id, creds } + return {uid: creds.data.user_id, creds} } case 'key': { const key = creds.data @@ -84,7 +101,7 @@ export const lookupUser = async (creds: Credentials): Promise => { if (!privateUser) { throw new APIError(401, `No private user exists with API key ${key}.`) } - return { uid: privateUser.id, creds: { privateUser, ...creds } } + return {uid: privateUser.id, creds: {privateUser, ...creds}} } default: throw new APIError(401, 'Invalid credential type.') @@ -109,45 +126,45 @@ export const validate = (schema: T, val: unknown) => { } } -export const jsonEndpoint = (fn: JsonHandler) => { - return async (req: Request, res: Response, next: NextFunction) => { - try { - res.status(200).json(await fn(req, res)) - } catch (e) { - next(e) - } - } -} - -export const authEndpoint = (fn: AuthedHandler) => { - return async (req: Request, res: Response, next: NextFunction) => { - try { - const authedUser = await lookupUser(await parseCredentials(req)) - res.status(200).json(await fn(req, authedUser, res)) - } catch (e) { - next(e) - } - } -} - -export const MaybeAuthedEndpoint = ( - fn: MaybeAuthedHandler -) => { - return async (req: Request, res: Response, next: NextFunction) => { - let authUser: AuthedUser | undefined = undefined - try { - authUser = await lookupUser(await parseCredentials(req)) - } catch { - // it's treated as an anon request - } - - try { - res.status(200).json(await fn(req, authUser, res)) - } catch (e) { - next(e) - } - } -} +// export const jsonEndpoint = (fn: JsonHandler) => { +// return async (req: Request, res: Response, next: NextFunction) => { +// try { +// res.status(200).json(await fn(req, res)) +// } catch (e) { +// next(e) +// } +// } +// } +// +// export const authEndpoint = (fn: AuthedHandler) => { +// return async (req: Request, res: Response, next: NextFunction) => { +// try { +// const authedUser = await lookupUser(await parseCredentials(req)) +// res.status(200).json(await fn(req, authedUser, res)) +// } catch (e) { +// next(e) +// } +// } +// } +// +// export const MaybeAuthedEndpoint = ( +// fn: MaybeAuthedHandler +// ) => { +// return async (req: Request, res: Response, next: NextFunction) => { +// let authUser: AuthedUser | undefined = undefined +// try { +// authUser = await lookupUser(await parseCredentials(req)) +// } catch { +// // it's treated as an anon request +// } +// +// try { +// res.status(200).json(await fn(req, authUser, res)) +// } catch (e) { +// next(e) +// } +// } +// } export type APIHandler = ( props: ValidatedAPIParams, @@ -161,7 +178,7 @@ export const typedEndpoint = ( name: N, handler: APIHandler ) => { - const { props: propSchema, authed: authRequired, method } = API[name] + const {props: propSchema, authed: authRequired, method} = API[name] return async (req: Request, res: Response, next: NextFunction) => { let authUser: AuthedUser | undefined = undefined @@ -195,7 +212,7 @@ export const typedEndpoint = ( // Convert bigint to number, b/c JSON doesn't support bigint. const convertedResult = deepConvertBigIntToNumber(result) - res.status(200).json(convertedResult ?? { success: true }) + res.status(200).json(convertedResult ?? {success: true}) } if (hasContinue) { diff --git a/backend/shared/src/firebase-utils.ts b/backend/shared/src/firebase-utils.ts index 588996c8..b48666ea 100644 --- a/backend/shared/src/firebase-utils.ts +++ b/backend/shared/src/firebase-utils.ts @@ -3,11 +3,12 @@ import {ENV_CONFIG} from "common/envs/constants"; export const getServiceAccountCredentials = () => { let keyPath = ENV_CONFIG.googleApplicationCredentials - // console.log('Using GOOGLE_APPLICATION_CREDENTIALS:', keyPath) + console.log('Using GOOGLE_APPLICATION_CREDENTIALS:', keyPath) if (!keyPath) { - throw new Error( - `Please set the GOOGLE_APPLICATION_CREDENTIALS environment variable to contain the path to your key file.` - ) + // throw new Error( + // `Please set the GOOGLE_APPLICATION_CREDENTIALS environment variable to contain the path to your key file.` + // ) + return {} } if (!keyPath.startsWith('/')) { diff --git a/backend/shared/src/init-admin.ts b/backend/shared/src/init-admin.ts index 08031c68..61ee9bde 100644 --- a/backend/shared/src/init-admin.ts +++ b/backend/shared/src/init-admin.ts @@ -9,9 +9,12 @@ export const initAdmin = () => { if (IS_LOCAL) { try { const serviceAccount = getServiceAccountCredentials() - console.log( - `Initializing connection to ${serviceAccount.project_id} Firebase...` - ) + // console.log(serviceAccount) + if (!serviceAccount.project_id) { + console.log(`GOOGLE_APPLICATION_CREDENTIALS not set, skipping admin firebase init.`) + return + } + console.log(`Initializing connection to ${serviceAccount.project_id} Firebase...`) return admin.initializeApp({ projectId: serviceAccount.project_id, credential: admin.credential.cert(serviceAccount), diff --git a/scripts/post_install.sh b/scripts/post_install.sh index de8dc46a..f94edb4c 100755 --- a/scripts/post_install.sh +++ b/scripts/post_install.sh @@ -7,4 +7,8 @@ cd "$(dirname "$0")"/.. if [ ! -f .env ]; then cp .env.example .env echo ".env file created from .env.example" -fi \ No newline at end of file +fi + +source .env + +openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 -in secrets/googleApplicationCredentials-dev.json.enc -out backend/shared/src/googleApplicationCredentials-dev.json -pass pass:$GOOGLE_CREDENTIALS_ENC_PWD \ No newline at end of file diff --git a/secrets/googleApplicationCredentials-dev.json.enc b/secrets/googleApplicationCredentials-dev.json.enc new file mode 100644 index 00000000..3656e544 Binary files /dev/null and b/secrets/googleApplicationCredentials-dev.json.enc differ