Skip to content
Merged
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
9 changes: 4 additions & 5 deletions core/lib/contracts/resources/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,18 @@ const UserSchema = z.object({
avatar: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
})
});

export type PubFieldsResponse = z.infer<typeof PubFieldsSchema>;
export type PubPostBody = z.infer<typeof PubPostSchema>;
export type SuggestedMember = z.infer<typeof SuggestedMembersSchema>;


export const integrationsApi = contract.router({
auth: {
body: z.object({
token: z.string(),
headers: z.object({
authorization: z.string(),
}),
method: "POST",
method: "GET",
path: "/integrations/:instanceId/auth",
summary: "Authenticate a user and receive basic information about them",
description:
Expand Down
5 changes: 3 additions & 2 deletions core/pages/api/v0/[...ts-rest].ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ const integrationsRouter = createNextRoute(api.integrations, {
body: member,
};
},
auth: async ({ headers, body, params }) => {
const user = await validateToken(body.token);
auth: async ({ headers }) => {
const token = headers.authorization.split("Bearer ")[1];
const user = await validateToken(token);
return {
status: 200,
body: user,
Expand Down
11 changes: 5 additions & 6 deletions integrations/submissions/app/actions/submit/actions.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
"use server";

import { Pub, makeClient } from "@pubpub/sdk";
import { assert, expect } from "utils";
import { Pub } from "@pubpub/sdk";
import manifest from "pubpub-integration.json";
import { assert, expect } from "utils";
import { client } from "~/lib/pubpub";
import { findInstance } from "~/lib/instance";

const client = makeClient(manifest);

export async function submit(form: FormData) {
export async function submit(form: FormData, token: string) {
try {
const { "instance-id": instanceId, ...pub } = Object.fromEntries(form);
assert(typeof instanceId === "string");
const instance = expect(await findInstance(instanceId));
return client.create(instanceId, pub as Pub<typeof manifest>, instance.pubTypeId);
return client.create(instanceId, token, pub as Pub<typeof manifest>, instance.pubTypeId);
} catch (error) {
return { error: error.message };
}
Expand Down
37 changes: 16 additions & 21 deletions integrations/submissions/app/actions/submit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
"use client";
import { client } from "~/lib/pubpub";
import { Submit } from "./submit";

import { useState, useTransition } from "react";
import { submit } from "./actions";
type Props = {
searchParams: {
instanceId: string;
token: string;
};
};

export default function Page(props: { searchParams: { instanceId: string } }) {
const { instanceId } = props.searchParams;
const [message, setMessage] = useState<string>("");
const [isPending, startTransition] = useTransition();

async function onSubmit(form: FormData) {
const response = await submit(form);
setMessage("error" in response ? response.error : "Pub submitted!");
}
export default async function Page(props: Props) {
const { instanceId, token } = props.searchParams;
const user = await client.auth(instanceId, token);

return (
<form action={(form) => startTransition(() => onSubmit(form))}>
<label>
<span>Title</span>
<input type="text" name="Title" />
</label>
<input type="hidden" name="instance-id" value={instanceId} />
<button type="submit">Submit</button>
<p>{isPending ? "Submitting Pub..." : message}</p>
</form>
<main>
<p>Hello {user.name}</p>
<img src={`${process.env.PUBPUB_URL}/${user.avatar}`} />
<Submit instanceId={instanceId} token={token} />
</main>
);
}
31 changes: 31 additions & 0 deletions integrations/submissions/app/actions/submit/submit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { useState, useTransition } from "react";
import { submit } from "./actions";

type Props = {
instanceId: string;
token: string;
};

export function Submit(props: Props) {
const [message, setMessage] = useState<string>("");
const [isPending, startTransition] = useTransition();

async function onSubmit(form: FormData) {
const response = await submit(form, props.token);
setMessage("error" in response ? response.error : "Pub submitted!");
}

return (
<form action={(form) => startTransition(() => onSubmit(form))}>
<label>
<span>Title</span>
<input type="text" name="Title" />
</label>
<input type="hidden" name="instance-id" value={props.instanceId} />
<button type="submit">Submit</button>
<p>{isPending ? "Submitting Pub..." : message}</p>
</form>
);
}
21 changes: 18 additions & 3 deletions integrations/submissions/app/configure/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { client } from "~/lib/pubpub";
import { findInstance } from "~/lib/instance";
import { Configure } from "./configure";

export default async function Page(props: { searchParams: { instanceId: string } }) {
const { instanceId } = props.searchParams;
type Props = {
searchParams: {
instanceId: string;
token: string;
};
};

export default async function Page(props: Props) {
const { instanceId, token } = props.searchParams;
const user = await client.auth(instanceId, token);
const instance = await findInstance(instanceId);
return <Configure instanceId={instanceId} pubTypeId={instance?.pubTypeId} />;
return (
<main>
<p>Hello {user.name}</p>
<img src={`${process.env.PUBPUB_URL}/${user.avatar}`} />
<Configure instanceId={instanceId} pubTypeId={instance?.pubTypeId} />
</main>
);
}
4 changes: 4 additions & 0 deletions integrations/submissions/lib/pubpub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { makeClient } from "@pubpub/sdk";
import manifest from "pubpub-integration.json";

export const client = makeClient(manifest);
46 changes: 38 additions & 8 deletions packages/sdk/src/lib/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { ValidationError, PubPubError, ResponseError, ZodError } from "./errors";
import { Manifest } from "./types";

// TODO: derive this type from core API contract
export type User = {
id: string;
slug: string;
email: string;
name: string;
avatar?: string | null;
createdAt: Date;
updatedAt: Date;
};

export type Get<T extends Manifest> = (
| Extract<keyof T["register"], string>
| Extract<keyof T["write"], string>
Expand Down Expand Up @@ -30,39 +41,45 @@ export type UpdateResponse<T extends string[]> = {
};

export type Client<T extends Manifest> = {
auth(instanceId: string, token: string): Promise<User>;
create<U extends string[]>(
instanceId: string,
token: string,
pub: Pub<T>,
pubTypeId: string
): Promise<UpdateResponse<U>>;
read<U extends Get<T>>(
instanceId: string,
token: string,
pubId: string,
...fields: U
): Promise<ReadResponse<U>>;
update<U extends string[]>(
instanceId: string,
token: string,
pubId: string,
patch: Patch<T>
): Promise<UpdateResponse<U>>;
};

const makeRequest = async (
instanceId: string,
method: string,
token: string,
method: "GET" | "POST" | "PATCH",
path?: string | null | undefined,
body?: unknown
) => {
const url = `${process.env.PUBPUB_URL}/api/v0/integrations/${instanceId}${
path ? `/${path}` : ""
}`;
const signal = AbortSignal.timeout(5000);
const headers = { "Content-Type": "application/json" };
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
const response = await fetch(url, {
method,
headers,
signal,
body: body ? JSON.stringify(body) : undefined,
cache: "no-store",
});
if (response.ok) {
return response.json();
Expand Down Expand Up @@ -100,41 +117,54 @@ export const makeClient = <T extends Manifest>(manifest: T): Client<T> => {
const write = new Set(manifest.write ? Object.keys(manifest.write) : null);
const read = new Set(manifest.read ? [write.values(), ...Object.keys(manifest.read)] : write);
return {
async create(instanceId, pub, pubTypeId) {
async auth(instanceId, token) {
try {
const userRaw = await makeRequest(instanceId, token, "GET", "auth");
const user = {
...userRaw,
createdAt: new Date(userRaw.createdAt),
updatedAt: new Date(userRaw.updatedAt),
};
return user;
} catch (cause) {
throw new PubPubError("Failed to authenticate user or integration", { cause });
}
},
async create(instanceId, token, pub, pubTypeId) {
for (let field in pub) {
if (!write.has(field)) {
throw new ValidationError(`Field ${field} is not writeable`);
}
}
try {
return makeRequest(instanceId, "POST", "pubs", {
return makeRequest(instanceId, token, "POST", "pubs", {
pubTypeId,
pubFields: pub,
});
} catch (cause) {
throw new PubPubError("Failed to create Pub", { cause });
}
},
async read(instanceId, pubId, ...fields) {
async read(instanceId, token, pubId, ...fields) {
try {
for (let i = 0; i < fields.length; i++) {
if (!read.has(fields[i])) {
throw new ValidationError(`Field ${fields[i]} is not readable`);
}
}
return makeRequest(instanceId, "GET", "pubs", pubId);
return makeRequest(instanceId, token, "GET", "pubs", pubId);
} catch (cause) {
throw new PubPubError("Failed to get Pub", { cause });
}
},
async update(instanceId, pubId, patch) {
async update(instanceId, token, pubId, patch) {
try {
for (const field in patch) {
if (!write.has(field)) {
throw new ValidationError(`Field ${field} is not writeable`);
}
}
return makeRequest(instanceId, "PATCH", `pubs/${pubId}`, patch);
return makeRequest(instanceId, token, "PATCH", `pubs/${pubId}`, patch);
} catch (cause) {
throw new PubPubError("Failed to update Pub", { cause });
}
Expand Down