Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into feature/single-account-mul…
Browse files Browse the repository at this point in the history
…tiple-athletes
  • Loading branch information
ikusteu committed Jan 27, 2024
2 parents 70f4baf + aa06c10 commit 48bddcb
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 200 deletions.
12 changes: 6 additions & 6 deletions common/autoinstallers/lint-staged/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions packages/client/src/__testUtils__/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
Auth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";

export const signInEmail = async (
auth: Auth,
email: string,
password: string
) => {
try {
return await createUserWithEmailAndPassword(auth, email, password);
} catch {
return await signInWithEmailAndPassword(auth, email, password);
}
};
88 changes: 25 additions & 63 deletions packages/client/src/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,59 @@
* @vitest-environment node
*/

import { describe, expect } from "vitest";
import { afterEach, describe, expect } from "vitest";
import {
httpsCallable,
HttpsCallableResult,
FunctionsError,
} from "@firebase/functions";

import { Collection, AuthStatus, HTTPSErrors } from "@eisbuk/shared";
import { AuthStatus, HTTPSErrors } from "@eisbuk/shared";
import { CloudFunction } from "@eisbuk/shared/ui";

import { adminDb, functions } from "@/__testSetup__/firestoreSetup";
import { adminDb, auth, functions } from "@/__testSetup__/firestoreSetup";

import { getCustomerDocPath } from "@/utils/firestore";

import { testWithEmulator } from "@/__testUtils__/envUtils";

import { saul } from "@eisbuk/testing/customers";
import { setUpOrganization } from "@/__testSetup__/node";
import { signInEmail } from "@/__testUtils__/auth";

describe("Test authentication", () => {
afterEach(() => {
auth.signOut();
});

describe("Test queryAuthStatus", () => {
const queryAuthStatus = (organization: string, authString: string) =>
const queryAuthStatus = (organization: string) =>
httpsCallable(
functions,
CloudFunction.QueryAuthStatus
)({ organization, authString }) as Promise<
HttpsCallableResult<AuthStatus>
>;

testWithEmulator(
"should successfully query admin status using email",
async () => {
// set up test state with saul as admin
const { organization } = await setUpOrganization();
await adminDb
.doc([Collection.Organizations, organization].join("/"))
.set({ admins: [saul.email] }, { merge: true });
const res = await queryAuthStatus(organization, saul.email!);
const {
data: { isAdmin },
} = res;
expect(isAdmin).toEqual(true);
}
);
)({ organization }) as Promise<HttpsCallableResult<AuthStatus>>;

testWithEmulator(
"single secretKey: should successfully query customer status using email",
async () => {
// set up test state with saul as customer, but not an admin
const { organization } = await setUpOrganization();
await adminDb.doc(getCustomerDocPath(organization, saul.id)).set(saul);
const {
data: { isAdmin, secretKeys },
} = await queryAuthStatus(organization, saul.email!);
expect(isAdmin).toEqual(false);
expect(secretKeys).toEqual([saul.secretKey]);
}
);

testWithEmulator(
"single secretKey: should successfully query customer status using phone",
async () => {
// set up test state with saul as customer, but not an admin
const { organization } = await setUpOrganization();
await adminDb.doc(getCustomerDocPath(organization, saul.id)).set(saul);
const {
data: { isAdmin, secretKeys },
} = await queryAuthStatus(organization, saul.phone!);
expect(isAdmin).toEqual(false);
expect(secretKeys).toEqual([saul.secretKey]);
}
);

testWithEmulator(
"multiple secretKeys: should return secretKeys for all customers with matching email",
async () => {
// set up test state with saul as customer, but not an admin
const { organization } = await setUpOrganization();
const jimmy = {
...saul,
id: "jimmy",
secretKey: "jimmy-secret",
};
await Promise.all([
adminDb.doc(getCustomerDocPath(organization, jimmy.id)).set(jimmy),
adminDb.doc(getCustomerDocPath(organization, saul.id)).set(saul),
// The auth string (email in this case) is read from the auth object in function context.
// Hence, the login.
signInEmail(auth, saul.email!, "password"),
]);
const {
data: { isAdmin, secretKeys },
} = await queryAuthStatus(organization, saul.email!);
} = await queryAuthStatus(organization);
expect(isAdmin).toEqual(false);
expect(secretKeys).toEqual([jimmy.secretKey, saul.secretKey]);
expect(secretKeys).toEqual([saul.secretKey]);
}
);

testWithEmulator(
"multiple secretKeys: should return secretKeys for all customers with matching phone number",
"multiple secretKeys: should return secretKeys for all customers with matching email",
async () => {
// set up test state with saul as customer, but not an admin
const { organization } = await setUpOrganization();
Expand All @@ -110,23 +66,29 @@ describe("Test authentication", () => {
await Promise.all([
adminDb.doc(getCustomerDocPath(organization, jimmy.id)).set(jimmy),
adminDb.doc(getCustomerDocPath(organization, saul.id)).set(saul),
// The auth string (email in this case) is read from the auth object in function context.
// Hence, the login (creating a user automatically loggs in).
signInEmail(auth, saul.email!, "password"),
]);
const {
data: { isAdmin, secretKeys },
} = await queryAuthStatus(organization, saul.phone!);
} = await queryAuthStatus(organization);
expect(isAdmin).toEqual(false);
expect(secretKeys).toEqual([jimmy.secretKey, saul.secretKey]);
}
);

// Note: There are no tests for phone as it's hard to test authentication with phone (due to recaptcha requirements)
// The desired behaviour, however, is tested using e2e tests (with full browser support).

testWithEmulator(
"should reject if no 'organization' or 'authString' provided",
"should reject if no 'organization' provided",
async () => {
try {
await httpsCallable(functions, CloudFunction.QueryAuthStatus)({});
} catch (error) {
expect((error as FunctionsError).message).toEqual(
`${HTTPSErrors.MissingParameter}: organization, authString`
`${HTTPSErrors.MissingParameter}: organization`
);
}
}
Expand Down
86 changes: 18 additions & 68 deletions packages/functions/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,73 +8,14 @@ import {
OrgSubCollection,
QueryAuthStatusPayload,
wrapIter,
DeprecatedAuthStatus,
} from "@eisbuk/shared";

import { wrapHttpsOnCallHandler } from "./sentry-serverless-firebase";
import { __functionsZone__ } from "./constants";
import { checkRequiredFields } from "./utils";

/**
* @deprecated This is hare for temporary backward compatibility, but is removed from
* 'CloutFunction' enum and is not used in the updated version of the app.
*
* @TODO Remove this function after allowing some time for update.
*/
export const queryAuthStatus = functions
.runWith({
timeoutSeconds: 20,
memory: "512MB",
})
.region(__functionsZone__)
.https.onCall(
wrapHttpsOnCallHandler(
"queryAuthStatus",
async (
payload: QueryAuthStatusPayload
): Promise<DeprecatedAuthStatus> => {
// validate payload
checkRequiredFields(payload, ["organization", "authString"]);

const { organization, authString } = payload;

const authStatus: DeprecatedAuthStatus = {
isAdmin: false,
};

const orgRef = admin
.firestore()
.collection(Collection.Organizations)
.doc(organization);
const customersRef = orgRef.collection(OrgSubCollection.Customers);

const [org, customers] = await Promise.all([
orgRef.get(),
customersRef.get(),
]);

// query admin status
const orgData = org.data() as OrganizationData;
if (orgData) {
authStatus.isAdmin = orgData.admins.includes(authString);
}

// query customer status
const authCustomer = customers.docs.find((customerDoc) => {
const data = customerDoc.data();
return data.email === authString || data.phone === authString;
});
if (authCustomer) {
authStatus.bookingsSecretKey = authCustomer.data().secretKey;
}

return authStatus;
}
)
);
import { wrapHttpsOnCallHandler } from "./sentry-serverless-firebase";
import { checkRequiredFields, checkUser } from "./utils";

/** @TODO Rename this to 'queryAuthStatus' once the deprecated function is removed. */
export const queryAuthStatus2 = functions
export const queryAuthStatus = functions
.runWith({
timeoutSeconds: 20,
// With these options, your minimum bill will be $4.54 in a 30-day month
Expand All @@ -85,12 +26,21 @@ export const queryAuthStatus2 = functions
.region(__functionsZone__)
.https.onCall(
wrapHttpsOnCallHandler(
"queryAuthStatus2",
async (payload: QueryAuthStatusPayload): Promise<AuthStatus> => {
// validate payload
checkRequiredFields(payload, ["organization", "authString"]);

const { organization, authString } = payload;
"queryAuthStatus",
async (
payload: QueryAuthStatusPayload,
{ auth }
): Promise<AuthStatus> => {
// Organization is required to even start the functionality
checkRequiredFields(payload, ["organization"]);
const { organization } = payload;

await checkUser(organization, auth);

// It's safe to cast this to non-null as the auth check has already been done
const { email, phone_number: phone } = auth!.token!;
// At least one of the two will be defined
const authString = (email || phone) as string;

const authStatus: AuthStatus = {
isAdmin: false,
Expand Down
Loading

0 comments on commit 48bddcb

Please sign in to comment.