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
44 changes: 37 additions & 7 deletions client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -590,14 +590,13 @@ function AccessControl({ artifact }: { artifact: ViewedArtifact }) {
);
}

function ArtifactFrame({ requestedSlug }: { requestedSlug?: string }) {
function ArtifactFrame({ slug }: { slug: string }) {
const auth = useAuth();
const params = useParams<{ slug: string }>();
const slug = cleanSlug(requestedSlug ?? params.slug ?? "");
const artifact = client.useQuery("artifactBySlug", slug);
const acceptArtifactAccess = client.useMutation("acceptArtifactAccess");
const accessBootstrap = useAccessBootstrap();
const [accessState, setAccessState] = useState<"idle" | "accepting" | "accepted" | "denied">("idle");
const [accessState, setAccessState] = useState<"idle" | "accepting" | "accepted" | "expired" | "denied">("idle");
const [expiredAt, setExpiredAt] = useState("");
const [copied, setCopied] = useState(false);

useEffect(() => {
Expand Down Expand Up @@ -628,7 +627,14 @@ function ArtifactFrame({ requestedSlug }: { requestedSlug?: string }) {

setAccessState("accepting");
void acceptArtifactAccess(slug)
.then((result) => setAccessState(result.accepted ? "accepted" : "denied"))
.then((result) => {
if (result.status === "expired") {
setExpiredAt(result.expiredAt);
setAccessState("expired");
return;
}
setAccessState(result.status === "accepted" ? "accepted" : "denied");
})
.catch(() => setAccessState("denied"));
}, [
artifact,
Expand All @@ -639,18 +645,40 @@ function ArtifactFrame({ requestedSlug }: { requestedSlug?: string }) {
accessBootstrap.state
]);

useEffect(() => {
if (artifact !== null || accessState !== "accepted") {
return;
}
const timeout = window.setTimeout(() => setAccessState("denied"), 5000);
return () => window.clearTimeout(timeout);
}, [artifact, accessState]);

if (artifact === undefined) {
return <main className="grid min-h-screen place-items-center text-slate-500">Opening artifact…</main>;
}
if (artifact === null) {
if (auth.isGuest) {
return <SignInCard shared />;
}
if (accessState === "expired") {
return (
<main className="mx-auto grid min-h-screen max-w-xl place-content-center px-6 py-24 text-center">
<p className="font-mono text-xs uppercase tracking-[0.22em] text-amber-300">Expired</p>
<h1 className="mt-4 text-3xl font-semibold text-white">This artifact has expired.</h1>
<p className="mt-4 text-slate-400">
It expired {formatDate(expiredAt)}. Ask the owner to republish it with a longer lifetime.
</p>
<div className="mt-7">
<Link className="rounded-lg border border-white/10 px-4 py-2 text-sm text-slate-300 hover:border-white/30" to="/">Back</Link>
</div>
</main>
);
}
if (
accessState === "accepting" ||
accessState === "accepted" ||
accessBootstrap.state === "claiming" ||
accessBootstrap.state === "claimed"
(accessBootstrap.state === "claimed" && accessState === "idle")
) {
return <main className="grid min-h-screen place-items-center text-slate-500">Verifying shared access…</main>;
}
Expand Down Expand Up @@ -734,7 +762,9 @@ function SignedInRoot() {
}

function ArtifactPage({ requestedSlug }: { requestedSlug?: string }) {
return <ArtifactFrame requestedSlug={requestedSlug} />;
const params = useParams<{ slug: string }>();
const slug = cleanSlug(requestedSlug ?? params.slug ?? "");
return <ArtifactFrame key={slug} slug={slug} />;
}

function AppContent() {
Expand Down
64 changes: 38 additions & 26 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ type PublishInput = {
expiresInSeconds?: number | null;
};

type ArtifactAccessResult =
| { status: "accepted" }
| { status: "expired"; expiredAt: string }
| { status: "unavailable" };

function ownerEmails(ctx: EnvironmentContext): string[] {
const configured = (ctx.env.OWNER_EMAILS ?? "")
.split(",")
Expand Down Expand Up @@ -617,27 +622,21 @@ export default capsule({
claimed: await claimConfiguredWorkspaceViewer(ctx)
})),

acceptArtifactAccess: mutation(async (ctx, slugInput: string) => {
acceptArtifactAccess: mutation(async (
ctx,
slugInput: string
): Promise<ArtifactAccessResult> => {
const identity = authenticatedIdentity(ctx);
if (!identity) {
return { accepted: false };
}
if (await claimConfiguredOwner(ctx)) {
return { accepted: true };
}
if (await claimConfiguredWorkspaceViewer(ctx)) {
return { accepted: true };
}

const slug = cleanSlug(slugInput);
const artifact = await ctx.db.artifacts
.withIndex("by_slug", (q) => q.eq("slug", slug))
.first();
if (!artifact || isArtifactExpired(artifact.expiresAt)) {
return { accepted: false };
if (!artifact) {
return { status: "unavailable" };
}
if (artifact.isPublic === true) {
return { accepted: true };

if (!identity) {
return { status: "unavailable" };
}

const sharedWith = parseSharedEmails(artifact.sharedWith);
Expand All @@ -649,26 +648,39 @@ export default capsule({
? "domain"
: null;
const ruleValue = ruleType === "email" ? identity.email : domain;
if (!ruleType) {
return { accepted: false };
const isOwner = await claimConfiguredOwner(ctx);
const isWorkspaceViewer = isOwner
? false
: await claimConfiguredWorkspaceViewer(ctx);
const hasGrant = await validArtifactGrant(
ctx,
artifact.id,
identity.userId,
sharedWith,
sharedDomains
);
const canView =
artifact.isPublic === true ||
isOwner ||
isWorkspaceViewer ||
hasGrant ||
ruleType !== null;
if (!canView) {
return { status: "unavailable" };
}
if (isArtifactExpired(artifact.expiresAt)) {
return { status: "expired", expiredAt: artifact.expiresAt };
}

const grants = await ctx.db.artifactGrants
.withIndex("by_artifact_user", (q) =>
q.eq("artifactId", artifact.id).eq("userId", identity.userId)
)
.collect();
if (!grants.some((grant) =>
grant.ruleType === ruleType && grant.ruleValue === ruleValue
)) {
if (!isOwner && !isWorkspaceViewer && !hasGrant && ruleType) {
await ctx.db.artifactGrants.insert({
artifactId: artifact.id,
userId: identity.userId,
ruleType,
ruleValue
});
}
return { accepted: true };
return { status: "accepted" };
}),

publishArtifact: mutation(async (ctx, input: PublishInput) => {
Expand Down