Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions scripts/src/createApiKeysCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { MongoClient } from "mongodb";
import config from "@api/config";

process.on("unhandledRejection", err => {
throw err;
});

const COLLECTION = "apikeys";

const validator = {
$jsonSchema: {
bsonType: "object",
required: ["hexathon", "provider", "key"],
properties: {
hexathon: { bsonType: "objectId" },
provider: { bsonType: "string" },
key: { bsonType: "string" },
hexathonUser: { bsonType: "objectId" },
claimedAt: { bsonType: "date" },
},
},
};

const createApiKeysCollection = async () => {
const client = new MongoClient(config.database.mongo.uri);
await client.connect();
const db = client.db(config.services.HEXATHONS.database.name);

const existing = await db.listCollections({ name: COLLECTION }).toArray();
if (existing.length === 0) {
await db.createCollection(COLLECTION, { validator });
console.log(`Created collection ${COLLECTION}`);
} else {
await db.command({ collMod: COLLECTION, validator });
console.log(`Updated validator on existing collection ${COLLECTION}`);
}

await db.collection(COLLECTION).createIndexes([
{ key: { key: 1 }, unique: true, name: "key_1" },
{ key: { hexathon: 1, provider: 1 }, name: "hexathon_1_provider_1" },
{
key: { hexathon: 1, provider: 1, hexathonUser: 1 },
unique: true,
partialFilterExpression: { hexathonUser: { $type: "objectId" } },
name: "hexathon_1_provider_1_hexathonUser_1",
},
]);
console.log("Created indexes");

await client.close();
};

createApiKeysCollection();
49 changes: 49 additions & 0 deletions services/hexathons/src/models/apiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { AccessibleRecordModel, accessibleRecordsPlugin } from "@casl/mongoose";
import mongoose, { Schema, model, Types } from "mongoose";

import { HexathonModel } from "./hexathon";
import { HexathonUserModel } from "./hexathonUser";

export interface ApiKey extends mongoose.Document {
hexathon: Types.ObjectId;
provider: string;
key: string;
hexathonUser?: Types.ObjectId;
claimedAt?: Date;
}

const apiKeySchema = new Schema<ApiKey>({
hexathon: {
type: Schema.Types.ObjectId,
required: true,
ref: HexathonModel,
},
provider: {
type: String,
required: true,
default: "openai",
},
key: {
type: String,
required: true,
unique: true,
},
hexathonUser: {
type: Schema.Types.ObjectId,
ref: HexathonUserModel,
},
claimedAt: {
type: Date,
},
});

apiKeySchema.index({ hexathon: 1, provider: 1 });

apiKeySchema.index(
{ hexathon: 1, provider: 1, hexathonUser: 1 },
{ unique: true, partialFilterExpression: { hexathonUser: { $type: "objectId" } } }
);

apiKeySchema.plugin(accessibleRecordsPlugin);

export const ApiKeyModel = model<ApiKey, AccessibleRecordModel<ApiKey>>("ApiKey", apiKeySchema);
2 changes: 2 additions & 0 deletions services/hexathons/src/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const addAbilities = (): RequestHandler => (req, res, next) => {
if (req.user.roles.admin || req.user.roles.exec) {
can("manage", "Hexathon");
can("manage", "SwagItem");
can("manage", "ApiKey");
}

if (req.user.roles.admin || req.user.roles.member) {
Expand All @@ -49,6 +50,7 @@ export const addAbilities = (): RequestHandler => (req, res, next) => {
can("read", "SponsorVisit");
can("read", "Block");
can("read", "FoodBatch");
can("read", "ApiKey");

req.ability = build();
next();
Expand Down
108 changes: 108 additions & 0 deletions services/hexathons/src/routes/api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import express from "express";
import { asyncHandler, BadRequestError, checkAbility } from "@api/common";
import { FilterQuery } from "mongoose";

import { ApiKey, ApiKeyModel } from "../models/apiKey";
import { HexathonUserModel } from "../models/hexathonUser";

export const apiKeyRouter = express.Router();

apiKeyRouter.route("/claim").post(
checkAbility("read", "ApiKey"),
asyncHandler(async (req, res) => {
if (!req.body.hexathon) {
throw new BadRequestError("Hexathon is required body field");
}

const filter = {
hexathon: String(req.body.hexathon),
provider: String(req.body.provider ?? "openai"),
};

const hexathonUser = await HexathonUserModel.findOne({
userId: req.user?.uid,
hexathon: filter.hexathon,
});
if (!hexathonUser) {
throw new BadRequestError("You are not checked in to this hexathon.");
}

const existing = await ApiKeyModel.findOne({ ...filter, hexathonUser: hexathonUser.id });
if (existing) {
return res.send(existing);
}

let claimed;
try {
claimed = await ApiKeyModel.findOneAndUpdate(
{ ...filter, hexathonUser: null },
{ hexathonUser: hexathonUser.id, claimedAt: new Date() },
{ new: true }
);
} catch (err: any) {
if (err.code !== 11000) {
throw err;
}
return res.send(await ApiKeyModel.findOne({ ...filter, hexathonUser: hexathonUser.id }));
}

if (!claimed) {
throw new BadRequestError("No API keys are available. Please contact an organizer.");
}
return res.send(claimed);
})
);

apiKeyRouter.route("/").get(
checkAbility("aggregate", "ApiKey"),
asyncHandler(async (req, res) => {
if (!req.query.hexathon) {
throw new BadRequestError("Hexathon is required parameter");
}

const filter: FilterQuery<ApiKey> = {
hexathon: String(req.query.hexathon),
};
if (req.query.provider) {
filter.provider = String(req.query.provider);
}
if (req.query.claimed === "true") {
filter.hexathonUser = { $type: "objectId" };
} else if (req.query.claimed === "false") {
filter.hexathonUser = null;
}

const keys = await ApiKeyModel.find(filter)
.populate("hexathonUser", "name email userId")
.sort({ claimedAt: -1 });
const providers = await ApiKeyModel.distinct("provider", {
hexathon: String(req.query.hexathon),
});
const claimed = keys.filter(apiKey => apiKey.hexathonUser).length;

return res.send({
total: keys.length,
claimed,
available: keys.length - claimed,
providers,
keys,
});
})
);

apiKeyRouter.route("/").post(
checkAbility("create", "ApiKey"),
asyncHandler(async (req, res) => {
const { hexathon, keys } = req.body;
if (!hexathon || !Array.isArray(keys) || keys.length === 0) {
throw new BadRequestError("Hexathon and a non-empty keys array are required body fields");
}

const provider = String(req.body.provider ?? "openai");
const created = await ApiKeyModel.insertMany(
keys.map((key: string) => ({ hexathon: String(hexathon), provider, key: String(key).trim() }))
);

return res.send({ inserted: created.length });
})
);
2 changes: 2 additions & 0 deletions services/hexathons/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { sponsorVisitRouter } from "./sponsor-visit";
import { blockRoutes } from "./block";
import { teamRoutes } from "./team";
import { foodBatchRouter } from "./food-batch";
import { apiKeyRouter } from "./api-key";

export const defaultRouter = express.Router();

Expand All @@ -27,3 +28,4 @@ defaultRouter.use("/sponsor-visit", sponsorVisitRouter);
defaultRouter.use("/blocks", blockRoutes);
defaultRouter.use("/teams", teamRoutes);
defaultRouter.use("/food-batch", foodBatchRouter);
defaultRouter.use("/api-keys", apiKeyRouter);
Loading