diff --git a/.gitignore b/.gitignore index cb87637a..4a58bbb5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ bin/ cache/ +.vim/ + # dependencies node_modules .pnp diff --git a/packages/core-firebase/src/admin/collection.ts b/packages/core-firebase/src/admin/collection.ts index f2ee8c5d..7b9625a4 100644 --- a/packages/core-firebase/src/admin/collection.ts +++ b/packages/core-firebase/src/admin/collection.ts @@ -31,6 +31,8 @@ import { erc721MintGroupPath, erc721MintPath, projectWarpConfigPath, + teamApiKeyGroupPath, + teamApiKeyPath, } from "../collections.js"; import { NetworkDataEncoded, @@ -58,6 +60,7 @@ import { NetworkId, ProjectWarpConfigData, } from "../models/index.js"; +import { TeamApiKeyData } from "../models/TeamApiKey.js"; //users export const userCol = getColRef(firestore, userPath); @@ -71,6 +74,10 @@ export const teamNetworkColGroup = getColGroupRef(firestore, export const teamNetworkCol = (collectionId: TeamId) => getColRef(firestore, teamNetworkPath(collectionId)); +export const teamApiKeyColGroup = getColGroupRef(firestore, teamApiKeyGroupPath); +export const teamApiKeyCol = (collectionId: TeamId) => + getColRef(firestore, teamApiKeyPath(collectionId)); + //project export const projectCol = getColRef(firestore, projectPath); export const projectApiKeyColGroup = getColGroupRef(firestore, projectApiKeyGroupPath); diff --git a/packages/core-firebase/src/admin/controllers/team.ts b/packages/core-firebase/src/admin/controllers/team.ts index 469ea661..438c0c59 100644 --- a/packages/core-firebase/src/admin/controllers/team.ts +++ b/packages/core-firebase/src/admin/controllers/team.ts @@ -3,7 +3,7 @@ import { isUUID } from "../../utils/uuid.js"; import { Team } from "../../models/index.js"; import { teamMemberResource, teamResource } from "../resources.js"; import { getTeamsFactory } from "../../controllers/index.js"; -import { teamMemberGroupQuery } from "../groupQueries.js"; +import { teamApiKeyGroupQuery, teamMemberGroupQuery } from "../groupQueries.js"; export const getTeams = getTeamsFactory(teamMemberGroupQuery, teamResource); @@ -47,3 +47,11 @@ export async function createTeam(team: Omit): Promise { return teamId; } + +export async function getTeamWithApiKey(apiKey: string): Promise { + const teamApiKey = await teamApiKeyGroupQuery.getWhereFirst({ apiKey }); + if (!teamApiKey) return null; + + const team = await teamResource.getOrNull({ teamId: teamApiKey.teamId }); + return team; +} diff --git a/packages/core-firebase/src/admin/groupQueries.ts b/packages/core-firebase/src/admin/groupQueries.ts index a43a66f4..08269dd2 100644 --- a/packages/core-firebase/src/admin/groupQueries.ts +++ b/packages/core-firebase/src/admin/groupQueries.ts @@ -10,6 +10,7 @@ import { projectUserWalletSafeColGroup, projectWalletDfnsColGroup, projectWalletSafeColGroup, + teamApiKeyColGroup, teamMemberColGroup, teamNetworkColGroup, } from "./collection.js"; @@ -72,6 +73,13 @@ import { encodeTeamMemberData, encodeTeamMemberDataPartial, } from "../models/index.js"; +import { + decodeTeamApiKeyId, + encodeTeamApiKeyData, + encodeTeamApiKeyDataPartial, + TeamApiKeyData, + TeamApiKeyId, +} from "../models/TeamApiKey.js"; //users //Search user team membership across teams @@ -102,6 +110,21 @@ export const teamNetworkGroupQuery = getFirebaseQueryResource< decodeParentDocId: decodeTeamId, }); +export const teamApiKeyGroupQuery = getFirebaseQueryResource< + TeamApiKeyData, + TeamApiKeyId, + TeamId, + TeamApiKeyData, + TeamApiKeyData, + Query<"admin", TeamApiKeyData> +>(teamApiKeyColGroup, { + decodeId: decodeTeamApiKeyId, + encodeDataPartial: encodeTeamApiKeyDataPartial, + decodeData: encodeTeamApiKeyData, + encodeParentDocId: encodeTeamId, + decodeParentDocId: decodeTeamId, +}); + //project //Search project api key across projects export const projectApiKeyGroupQuery = getFirebaseQueryResource< diff --git a/packages/core-firebase/src/admin/resources.ts b/packages/core-firebase/src/admin/resources.ts index 6a05531c..bb227d7b 100644 --- a/packages/core-firebase/src/admin/resources.ts +++ b/packages/core-firebase/src/admin/resources.ts @@ -25,6 +25,7 @@ import { projectWalletDfnsCol, projectWalletSafeCol, projectWarpConfigCol, + teamApiKeyCol, teamCol, teamMemberCol, teamNetworkCol, @@ -130,6 +131,12 @@ import { encodeProjectWarpConfigData, encodeProjectWarpConfigDataPartial, encodeProjectWarpConfigId, + TeamApiKeyData, + TeamApiKeyId, + encodeTeamApiKeyId, + decodeTeamApiKeyId, + encodeTeamApiKeyDataPartial, + encodeTeamApiKeyData, } from "../models/index.js"; //user & team @@ -169,6 +176,15 @@ export const teamNetworkResource = getFirebaseResource< encodeParentDocId: encodeTeamId, decodeParentDocId: decodeTeamId, }); +export const teamApiKeyResource = getFirebaseResource(firestore, teamApiKeyCol, { + encodeId: encodeTeamApiKeyId, + decodeId: decodeTeamApiKeyId, + encodeDataPartial: encodeTeamApiKeyDataPartial, + encodeData: encodeTeamApiKeyData, + encodeParentDocId: encodeTeamId, + decodeParentDocId: decodeTeamId, +}); + //project export const projectResource = getFirebaseResource(firestore, projectCol, { encodeId: encodeProjectId, diff --git a/packages/core-firebase/src/collections.ts b/packages/core-firebase/src/collections.ts index 5572e791..710210ef 100644 --- a/packages/core-firebase/src/collections.ts +++ b/packages/core-firebase/src/collections.ts @@ -23,10 +23,16 @@ export const teamMemberGroupPath = "teamMember"; export const teamMemberPath = (collectionId: TeamId) => { return join(teamPath, encodeTeamId(collectionId), teamMemberGroupPath); }; +//team export const teamNetworkGroupPath = "teamNetwork"; export const teamNetworkPath = (collectionId: TeamId) => { return join(teamPath, encodeTeamId(collectionId), teamNetworkGroupPath); }; +export const teamApiKeyGroupPath = "teamApiKey"; +export const teamApiKeyPath = (collectionId: TeamId) => { + return join(teamPath, encodeTeamId(collectionId), teamApiKeyGroupPath); +}; + //project export const projectPath = "project"; export const projectApiKeyGroupPath = "projectApiKey"; diff --git a/packages/core-firebase/src/models/TeamApiKey.ts b/packages/core-firebase/src/models/TeamApiKey.ts new file mode 100644 index 00000000..f94b850a --- /dev/null +++ b/packages/core-firebase/src/models/TeamApiKey.ts @@ -0,0 +1,63 @@ +import { TypeOf, expectType } from "ts-expect"; +import { z } from "zod"; +import { + FirestoreSDK, + FirebaseQueryResource, + Query, + FirebaseResource, + FieldOverridesSchema, +} from "@owlprotocol/crud-firebase"; +import { TeamId } from "./Team.js"; + +export interface TeamApiKeyId { + readonly apiKey: string; +} +export const teamApiKeyIdZod = z + .union([z.string(), z.object({ apiKey: z.string() })]) + .transform((arg) => (typeof arg === "string" ? arg : arg.apiKey)); +export const encodeTeamApiKeyId: (id: string | TeamApiKeyId) => string = teamApiKeyIdZod.parse; +export const decodeTeamApiKeyId: (id: string) => Required = (id) => { + return { apiKey: id }; +}; + +export interface TeamApiKeyData { + readonly apiKey: string; + readonly createdAt?: number; + readonly expiresAt?: number; +} + +export const teamApiKeyDataZod = z + .object({ + apiKey: z.string(), + createdAt: z.number().int().positive().describe("timestamp of team api key creation").optional(), + expiresAt: z.number().int().positive().describe("expiry").optional(), + }) + .describe("team api key"); +export const encodeTeamApiKeyData: (data: TeamApiKeyData) => TeamApiKeyData = teamApiKeyDataZod.parse; +export const encodeTeamApiKeyDataPartial: (data: Partial) => Partial = + teamApiKeyDataZod.partial().parse; + +export type TeamApiKey = Required & TeamApiKeyData; +//Generic interfaces for resource, useful for writing logic that works both in firebase admin/web +export type TeamApiKeyResource = FirebaseResource; +//Generic interfaces for read resource, and group read resource (for subcollections) +export type TeamApiKeyQueryResource = FirebaseQueryResource; +export type TeamApiKeyGroupQueryResource = FirebaseQueryResource< + FirestoreSDK, + TeamApiKeyData, + TeamApiKeyId, + TeamId, + TeamApiKeyData, + TeamApiKeyData, + Query +>; + +//Check zod validator matches interface +expectType>>(true); +expectType>>(true); + +export const TeamApiKeyFieldOverrides: FieldOverridesSchema = { + apiKey: "COLLECTION_GROUP", + expiresAt: "IGNORE", + createdAt: "IGNORE", +}; diff --git a/packages/core-firebase/src/models/index.ts b/packages/core-firebase/src/models/index.ts index 762181f0..4a2513de 100644 --- a/packages/core-firebase/src/models/index.ts +++ b/packages/core-firebase/src/models/index.ts @@ -2,6 +2,7 @@ export * from "./projects/index.js"; export * from "./common.js"; export * from "./Team.js"; +export * from "./TeamApiKey.js"; export * from "./TeamMember.js"; export * from "./TeamNetwork.js"; export * from "./ERC721Mint.js"; diff --git a/packages/core-firebase/src/query/queryOptions.ts b/packages/core-firebase/src/query/queryOptions.ts index 8c09ef09..a904da7d 100644 --- a/packages/core-firebase/src/query/queryOptions.ts +++ b/packages/core-firebase/src/query/queryOptions.ts @@ -56,6 +56,7 @@ import { projectWalletDfnsGroupPath, projectWalletSafeGroupPath, projectWarpConfigGroupPath, + teamApiKeyGroupPath, } from "../collections.js"; import { projectApiKeyGroupQuery, @@ -63,8 +64,10 @@ import { projectUserGroupQuery, projectUserWalletDfnsGroupQuery, projectUserWalletSafeGroupQuery, + teamApiKeyGroupQuery, teamMemberGroupQuery, } from "../web/groupQueries.js"; +import { TeamApiKeyData, TeamApiKeyId } from "../models/TeamApiKey.js"; /*** Collection Queries ***/ //TODO: REQUIRED Replace prefixPath/collectionGroup with computed => need getWhere to support getColPath() @@ -215,6 +218,14 @@ export const teamMemberGroupQueryOptions = getFirebaseQueryReactQueryOptions< TeamMemberData, Query<"web", TeamMemberData> >(teamMemberGroupQuery, { prefixPath: [], collectionGroup: teamMemberGroupPath }); +export const teamApiKeyGroupQueryOptions = getFirebaseQueryReactQueryOptions< + TeamApiKeyData, + TeamApiKeyId, + TeamId, + TeamApiKeyData, + TeamApiKeyData, + Query<"web", TeamApiKeyData> +>(teamApiKeyGroupQuery, { prefixPath: [], collectionGroup: teamApiKeyGroupPath }); //project export const projectApiKeyGroupQueryOptions = getFirebaseQueryReactQueryOptions< diff --git a/packages/core-firebase/src/scripts/createTeamApiKey.ts b/packages/core-firebase/src/scripts/createTeamApiKey.ts new file mode 100644 index 00000000..63916fa8 --- /dev/null +++ b/packages/core-firebase/src/scripts/createTeamApiKey.ts @@ -0,0 +1,18 @@ +import { v4 as uuidv4 } from "uuid"; +import { teamApiKeyResource } from "../admin/resources.js"; + +export async function createTeamApiKey() { + if (process.argv.length != 3) throw new Error("Usage: node createTeamApiKey.js "); + const teamId = process.argv[2]; + + const apiKey = uuidv4(); + + const numTeamApiKeys = await teamApiKeyResource.getWhereCount({ teamId }); + + if (numTeamApiKeys > 0) { + throw new Error("Team already has an API key"); + } + await teamApiKeyResource.set({ teamId, apiKey, createdAt: Date.now() }); +} + +createTeamApiKey().then(() => console.log("Done")); diff --git a/packages/core-firebase/src/web/collection.ts b/packages/core-firebase/src/web/collection.ts index c99a0652..a033c676 100644 --- a/packages/core-firebase/src/web/collection.ts +++ b/packages/core-firebase/src/web/collection.ts @@ -29,6 +29,8 @@ import { erc721MintGroupPath, erc721MintPath, projectWarpConfigPath, + teamApiKeyGroupPath, + teamApiKeyPath, } from "../collections.js"; import { ERC721MintData, @@ -55,6 +57,7 @@ import { TeamMemberData, UserData, } from "../models/index.js"; +import { TeamApiKeyData } from "../models/TeamApiKey.js"; //users export const userCol = getColRef(firestore, userPath); @@ -68,6 +71,10 @@ export const teamNetworkColGroup = getColGroupRef(firestore, export const teamNetworkCol = (collectionId: TeamId) => getColRef(firestore, teamNetworkPath(collectionId)); +export const teamApiKeyColGroup = getColGroupRef(firestore, teamApiKeyGroupPath); +export const teamApiKeyCol = (collectionId: TeamId) => + getColRef(firestore, teamApiKeyPath(collectionId)); + //project export const projectCol = getColRef(firestore, projectPath); export const projectApiKeyColGroup = getColGroupRef(firestore, projectApiKeyGroupPath); diff --git a/packages/core-firebase/src/web/groupQueries.ts b/packages/core-firebase/src/web/groupQueries.ts index 87d8164f..61fdd646 100644 --- a/packages/core-firebase/src/web/groupQueries.ts +++ b/packages/core-firebase/src/web/groupQueries.ts @@ -9,6 +9,7 @@ import { projectUserWalletSafeColGroup, projectWalletDfnsColGroup, projectWalletSafeColGroup, + teamApiKeyColGroup, teamMemberColGroup, teamNetworkColGroup, } from "./collection.js"; @@ -71,6 +72,13 @@ import { encodeTeamMemberData, encodeTeamMemberDataPartial, } from "../models/index.js"; +import { + decodeTeamApiKeyId, + encodeTeamApiKeyData, + encodeTeamApiKeyDataPartial, + TeamApiKeyData, + TeamApiKeyId, +} from "../models/TeamApiKey.js"; //users //Search user team membership across teams @@ -102,6 +110,21 @@ export const teamNetworkGroupQuery = getFirebaseQueryResource< decodeParentDocId: decodeTeamId, }); +export const teamApiKeyGroupQuery = getFirebaseQueryResource< + TeamApiKeyData, + TeamApiKeyId, + TeamId, + TeamApiKeyData, + TeamApiKeyData, + Query<"web", TeamApiKeyData> +>(teamApiKeyColGroup, { + decodeId: decodeTeamApiKeyId, + encodeDataPartial: encodeTeamApiKeyDataPartial, + decodeData: encodeTeamApiKeyData, + encodeParentDocId: encodeTeamId, + decodeParentDocId: decodeTeamId, +}); + //project //Search project api key across projects export const projectApiKeyGroupQuery = getFirebaseQueryResource< diff --git a/packages/core-firebase/src/web/resources.ts b/packages/core-firebase/src/web/resources.ts index 326c40c8..b545a005 100644 --- a/packages/core-firebase/src/web/resources.ts +++ b/packages/core-firebase/src/web/resources.ts @@ -24,6 +24,7 @@ import { projectWalletDfnsCol, projectWalletSafeCol, projectWarpConfigCol, + teamApiKeyCol, teamCol, teamMemberCol, teamNetworkCol, @@ -124,6 +125,14 @@ import { encodeProjectWarpConfigDataPartial, encodeProjectWarpConfigId, } from "../models/index.js"; +import { + decodeTeamApiKeyId, + encodeTeamApiKeyData, + encodeTeamApiKeyDataPartial, + encodeTeamApiKeyId, + TeamApiKeyData, + TeamApiKeyId, +} from "../models/TeamApiKey.js"; //user & team export const userResource = getFirebaseResource(firestore, userCol, { @@ -162,6 +171,15 @@ export const teamNetworkResource = getFirebaseResource< encodeParentDocId: encodeTeamId, decodeParentDocId: decodeTeamId, }); +export const teamApiKeyResource = getFirebaseResource(firestore, teamApiKeyCol, { + encodeId: encodeTeamApiKeyId, + decodeId: decodeTeamApiKeyId, + encodeDataPartial: encodeTeamApiKeyDataPartial, + encodeData: encodeTeamApiKeyData, + encodeParentDocId: encodeTeamId, + decodeParentDocId: decodeTeamId, +}); + //project export const projectResource = getFirebaseResource(firestore, projectCol, { encodeId: encodeProjectId,