Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
vsoraas committed Aug 7, 2024
2 parents 27ef535 + 5ef104f commit 2c6d41b
Show file tree
Hide file tree
Showing 52 changed files with 2,538 additions and 147 deletions.
36 changes: 36 additions & 0 deletions apps/api/src/auth/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { verifyKey } from '@unkey/api';
const DEBUG = false; // NB! Change to false before committing.
const FE_KEY = "super-secret"; // TODO: get from wrangler.toml
export async function verifyApiKey(key, dummy = false) {
if (DEBUG) {
console.debug("Middleware debug - processing key:", key);
}
if (dummy) {
const res = false;
if (DEBUG) {
console.debug("Middleware debug - returning API verified as:", res);
}
return res;
}
const { result, error } = await verifyKey(key);
if (DEBUG) {
console.debug("Middleware debug - verify-key results:", result?.code, result?.valid);
}
if (result) {
return result.valid;
}
else if (error) {
console.error("Couldn't verify key:", error.code, error.message);
}
return false;
}
export async function verifyFrontend(pass) {
if (DEBUG) {
console.debug("Middleware debug - processing pass:", pass);
}
const res = (pass == FE_KEY);
if (DEBUG) {
console.debug("Middleware debug - returning API verified as:", res);
}
return res;
}
66 changes: 66 additions & 0 deletions apps/api/src/auth/unkey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// const UNKEY_ROOT = process.env.UNKEY_ROOT
// const API_ID = process.env.UNKEY_API_ID
async function createAPIKey(rk, aId, workspaceId, serviceName = "", prefix = "") {
const options = {
method: 'POST',
headers: { Authorization: `Bearer ${rk}`, 'Content-Type': 'application/json' },
body: `
{"apiId":${aId},
"prefix":${prefix},
"ownerId":${workspaceId},
"name":${serviceName},
"meta":{
"ratelimit":{
"type":"fast",
"limit":10,
"duration":60000
},
"enabled":true,
}`
};
try {
const res = await fetch('https://api.unkey.dev/v1/keys.createKey', options);
const result = await res.json();
console.debug(`Tried to create API key for workspace <${workspaceId}>, returned with status <${res.status}>`);
return result.key;
}
catch (error) {
console.error(`${error.code}: ${error.message}`);
return null;
}
}
export { createAPIKey };
{ /*
body: `
{"apiId":${API_ID},
"prefix":"<string>",
"name":"my key",
"byteLength":135,
"ownerId":"team_123",
"meta":{
"billingTier":"PRO",
"trialEnds":"2023-06-16T17:16:37.161Z"},
"roles":[
"admin",
"finance"
],
"permissions":[
"domains.create_record",
"say_hello"
],
"expires":1623869797161,
"remaining":1000,
"refill":{
"interval":"daily",
"amount":100
},
"ratelimit":{
"type":"fast",
"limit":10,
"duration":60000
},
"enabled":true,
"environment":"<string>"
}`
*/
}
12 changes: 12 additions & 0 deletions apps/api/src/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { z } from "zod";
export const zEnv = z.object({
DATABASE_URL: z.string(),
ENVIRONMENT: z
.enum(["development", "preview", "production"])
.default("development"),
PO_ROOT: z.string(),
PO_SUB_KEY: z.string(),
PO_APP_KEY: z.string(),
PO_ONBOARD_REDIRECT: z.string(),
});
21 changes: 21 additions & 0 deletions apps/api/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { zEnv } from "./env";
import internal from "./routes/internal";
import external from "./routes/external";
import { honoFactory } from "./lib/hono";
const app = honoFactory();
// Main-level routes
app.route("/api/external", external);
app.route("/api/internal", internal);
export default {
fetch: (req, env, exCtx) => {
const parsedEnv = zEnv.safeParse(env);
if (!parsedEnv.success) {
return Response.json({
code: "BAD_ENVIRONMENT",
message: "Some environment variables are missing or are invalid",
errors: parsedEnv.error,
}, { status: 500 });
}
return app.fetch(req, parsedEnv.data, exCtx);
},
};
23 changes: 23 additions & 0 deletions apps/api/src/lib/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { PrismaClient, PrismaNeon, Pool } from "@dingify/db";
const pool = (env) => new Pool({ connectionString: env.DATABASE_URL });
const adapter = (env) => new PrismaNeon(pool(env));
const createPrismaClient = (env) => {
// Check if prisma client is already instantiated in global context
const globalPrisma = globalThis;
const existingPrismaClient = globalPrisma.prisma;
if (existingPrismaClient) {
return existingPrismaClient;
}
const prismaClient = new PrismaClient({
adapter: adapter(env),
log: env.ENVIRONMENT === "development"
? ["error", "warn"]
: ["error"],
errorFormat: "pretty",
});
if (env.ENVIRONMENT !== "production") {
globalPrisma.prisma = prismaClient;
}
return prismaClient;
};
export const prisma = (env) => createPrismaClient(env);
22 changes: 22 additions & 0 deletions apps/api/src/lib/dbExtension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Prisma } from '@prisma/client';
/**
* Extends Prisma with a filter on all queries such that the result will return only
* instances belonging to the same workspace as the user.
*
* Requires `user` to have a workspace, and the query must target a model that has
* `workspaceId` in its schema. The query must additionally not have `workspaceId`
* in its `where`-clause already.
*/
function workspaceExtension(user) {
const workspaceId = user.workspaceId;
const ext = Prisma.defineExtension({
query: {
$allOperations({ model, operation, args, query }) {
args.where = { ...args.where, workspaceId: workspaceId };
return query(args);
}
}
});
return ext;
}
export { workspaceExtension };
5 changes: 5 additions & 0 deletions apps/api/src/lib/generateApiKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function generateApiKey() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
}
6 changes: 6 additions & 0 deletions apps/api/src/lib/hono.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Hono } from "hono";
function honoFactory() {
const app = new Hono();
return app;
}
export { honoFactory };
98 changes: 98 additions & 0 deletions apps/api/src/lib/localApiKeys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { prisma } from "../lib/db";
async function getUserApiKeyFull(env, userId, serviceName) {
const db = prisma(env);
let apiKey;
try {
apiKey = await db.userApiKey.findFirst({
where: {
userId: userId,
serviceName: serviceName,
}
});
}
catch (error) {
console.log("Error fetching user API key:", error);
return null;
}
return apiKey;
}
async function getUserApiKey(env, userId, serviceName) {
const fullKey = await getUserApiKeyFull(env, userId, serviceName);
let res = null;
if (fullKey) {
res = fullKey.secret;
}
return res;
}
async function getWSApiKeyFull(env, workspaceId, serviceName) {
const db = prisma(env);
let apiKey;
try {
apiKey = await db.wSApiKey.findFirst({
where: {
workspaceId: workspaceId,
serviceName: serviceName,
}
});
}
catch (error) {
console.log("Error fetching user API key:", error);
return null;
}
return apiKey;
}
async function getWorkspaceApiKey(env, workspaceId, serviceName) {
const fullKey = await getWSApiKeyFull(env, workspaceId, serviceName);
let res = null;
if (fullKey) {
res = fullKey.secret;
}
return res;
}
async function storeWorkspaceAccessToken(db, workspaceId, serviceName, token, expiry) {
const now = new Date();
const expiryTime = (expiry * 1000) - 5000; // Shave 5 seconds off of the duration to compensate for roundtrip + db latency
const saveTime = new Date(now.getTime() + expiryTime);
try {
await db.workspaceAccessToken.upsert({
where: {
workspaceId_serviceName: {
workspaceId: workspaceId,
serviceName: serviceName,
}
},
update: {
secret: token,
validTo: saveTime,
},
create: {
workspaceId: workspaceId,
serviceName: serviceName,
secret: token,
validTo: saveTime,
}
});
}
catch (error) {
console.log(`Error saving access token (${serviceName}:${workspaceId}):`, error);
}
}
async function getWorkspaceAccessToken(db, workspaceId, serviceName) {
let accessToken = await db.workspaceAccessToken.findFirst({
where: {
workspaceId: workspaceId,
serviceName: serviceName,
}
});
if (!accessToken) {
console.debug("Returning null");
return null;
}
const now = new Date();
const adjustedTime = new Date(now.getTime() + 5000);
if (accessToken.validTo > adjustedTime) {
return accessToken;
}
return null;
}
export { getUserApiKey, getWorkspaceApiKey, getWorkspaceAccessToken, storeWorkspaceAccessToken, };
16 changes: 16 additions & 0 deletions apps/api/src/lib/parsePrismaError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function parsePrismaError(error) {
// A simple error message parser that looks for missing arguments
const missingArgumentMatch = error.message.match(/Argument `(\w+)` is missing./);
if (missingArgumentMatch) {
let fieldName = missingArgumentMatch[1];
let baseMessage = `The '${fieldName}' field is required but was not provided.`;
// Specific instructions for known fields
if (fieldName === "channel") {
baseMessage += " You need to add 'channel' to your call to make it work.";
}
// Add more specific messages for other fields if necessary
return baseMessage;
}
// Default to returning the original error message if no known patterns are matched
return error.message;
}
35 changes: 35 additions & 0 deletions apps/api/src/lib/poweroffice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getRequestHeaders } from "@/lib/poweroffice/auth";
async function superget(env, url, workspaceId) {
const poHeaders = await getRequestHeaders(env, workspaceId);
const response = await fetch(url, {
method: "GET",
headers: poHeaders,
});
if (!response.ok) {
throw new Error(`Bad response while fetching ${url}: ${response.status} ${response.statusText}`);
}
const res = await response.json();
return res;
}
async function superpost(env, url, workspaceId, data) {
const poHeaders = await getRequestHeaders(env, workspaceId);
let response;
try {
response = await fetch(url, {
method: "POST",
headers: poHeaders,
body: JSON.stringify(data),
});
}
catch (error) {
console.error("Superpost error:", error);
return;
}
if (!response.ok) {
console.error(`Bad response while posting to ${url}: ${response.status} ${response.statusText} ${await response.text()}`);
throw new Error(`Bad response while posting to ${url}: ${response.status} ${response.statusText}`);
}
const res = await response.json();
return res;
}
export { superget, superpost, };
Loading

0 comments on commit 2c6d41b

Please sign in to comment.