Skip to content

Commit

Permalink
with protect and redirect components
Browse files Browse the repository at this point in the history
  • Loading branch information
eluce2 committed Nov 13, 2024
1 parent 84cd230 commit c2b4b06
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 39 deletions.
83 changes: 66 additions & 17 deletions cli/src/cli/ottofms.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as clack from "@clack/prompts";
import axios from "axios";
import axios, { AxiosError } from "axios";
import chalk from "chalk";
import open from "open";
import randomstring from "randomstring";

import { abortIfCancel } from "./utils.js";

interface WizardResponse {
token: string;
}
Expand Down Expand Up @@ -144,23 +146,70 @@ export async function createDataAPIKey({
clack.log.info(
`${chalk.cyan("Creating a Data API Key")}\nEnter FileMaker credentials for ${chalk.bold(filename)}.\n${chalk.dim(`The account must have the fmrest extended privilege enabled.`)}`
);
const username = await clack.text({
message: `Enter the account name for ${chalk.bold(filename)}`,
});

const password = await clack.password({
message: `Enter the password for ${chalk.bold(filename)}`,
});
while (true) {
const username = abortIfCancel(
await clack.text({
message: `Enter the account name for ${chalk.bold(filename)}`,
})
);

const password = abortIfCancel(
await clack.password({
message: `Enter the password for ${chalk.bold(username)}`,
})
);

try {
const response = await axios.post<CreateAPIKeyResponse>(
`${url.origin}/otto/api/api-key/create-only`,
{
database: filename,
label: "For FM Web App",
user: username,
pass: password,
}
);

const response = await axios.post<CreateAPIKeyResponse>(
`${url.origin}/otto/api/api-key/create-only`,
{
database: filename,
label: "For FM Web App",
user: username,
pass: password,
return { apiKey: response.data.response.key };
} catch (error) {
if (!(error instanceof AxiosError)) {
clack.log.error(
`${chalk.red("Error creating Data API key:")} Unknown error`
);
} else {
const respMsg =
error.response?.data && "messages" in error.response.data
? (error.response.data as { messages?: { text?: string }[] })
.messages?.[0]?.text
: undefined;

clack.log.error(
`${chalk.red("Error creating Data API key:")} ${
respMsg ?? `Error code ${error.response?.status}`
}
${chalk.dim(
error.response?.status === 400 &&
`Common reasons this might happen:
- The provided credentials are incorrect.
- The account does not have the fmrest extended privilege enabled.
You may also want to try to create an API directly in the OttoFMS dashboard:
${url.origin}/otto/app/api-keys`
)}
`
);
}
const tryAgain = abortIfCancel(
await clack.confirm({
message: "Do you want to try and enter credentials again?",
active: "Yes, try again",
inactive: "No, abort",
})
);
if (!tryAgain) {
throw new Error("User cancelled");
}
}
);

return { apiKey: response.data.response.key };
}
}
7 changes: 5 additions & 2 deletions cli/src/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ Please run " ${npmName} init" first, or try this command again when inside a Pro
};

export class UserAbortedError extends Error {}

export function abortIfCancel(value: string | symbol): string {
export function abortIfCancel(value: symbol | string): string;
export function abortIfCancel<T extends boolean>(value: symbol | T): T;
export function abortIfCancel<T extends string | boolean>(
value: T | symbol
): T {
if (isCancel(value)) {
cancel();
throw new UserAbortedError();
Expand Down
2 changes: 1 addition & 1 deletion cli/src/generators/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ async function addProofkitAuth({
}: {
emailProvider?: "plunk" | "resend";
}) {
await proofkitAuthInstaller({ emailProvider });
await proofkitAuthInstaller();
mergeSettings({ auth: { type: "proofkit" } });
}
5 changes: 3 additions & 2 deletions cli/src/helpers/installDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from "chalk";
import { execa, type StdoutStderrOption } from "execa";
import ora, { type Ora } from "ora";

import { state } from "~/state.js";
import {
getUserPkgManager,
type PackageManager,
Expand Down Expand Up @@ -77,9 +78,9 @@ const runInstallCommand = async (
};

export const installDependencies = async ({
projectDir,
projectDir = state.projectDir,
}: {
projectDir: string;
projectDir?: string;
}) => {
logger.info("Installing dependencies...");
const pkgManager = getUserPkgManager();
Expand Down
4 changes: 3 additions & 1 deletion cli/src/installers/dependencyVersionMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const dependencyVersionMap = {
"@clerk/themes": "^2.1.33",

// FileMaker Data API
"@proofgeist/fmdapi": "^4.2.0",
"@proofgeist/fmdapi": "^4.2.1",

// ProofKit
"@proofgeist/kit": `^${getVersion()}`,
Expand All @@ -60,6 +60,8 @@ export const dependencyVersionMap = {
"@oslojs/binary": "^1.0.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"js-cookie": "^3.0.5",
"@types/js-cookie": "^3.0.6",

// React Email
"@react-email/components": "^0.0.28",
Expand Down
43 changes: 36 additions & 7 deletions cli/src/installers/proofkit-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import path from "path";
import { type OttoAPIKey } from "@proofgeist/fmdapi";
import chalk from "chalk";
import dotenv from "dotenv";
import { execa } from "execa";
import fs from "fs-extra";
import { type SourceFile } from "ts-morph";
import { SyntaxKind, type SourceFile } from "ts-morph";

import { getLayouts } from "~/cli/fmdapi.js";
import { PKG_ROOT } from "~/consts.js";
Expand All @@ -20,11 +19,7 @@ import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
import { addToHeaderSlot } from "./auth-shared.js";
import { installReactEmail } from "./react-email.js";

export const proofkitAuthInstaller = async ({
emailProvider,
}: {
emailProvider?: "plunk" | "resend";
}) => {
export const proofkitAuthInstaller = async () => {
const projectDir = state.projectDir;
addPackageDependency({
projectDir,
Expand All @@ -33,10 +28,17 @@ export const proofkitAuthInstaller = async ({
"@oslojs/binary",
"@oslojs/crypto",
"@oslojs/encoding",
"js-cookie",
],
devMode: false,
});

addPackageDependency({
projectDir,
dependencies: ["@types/js-cookie"],
devMode: true,
});

// copy all files from template/extras/proofkit-auth to projectDir/src
await fs.copy(
path.join(PKG_ROOT, "template/extras/proofkit-auth"),
Expand Down Expand Up @@ -106,6 +108,12 @@ export const proofkitAuthInstaller = async ({
});
await installReactEmail({ project });

protectMainLayout(
project.addSourceFileAtPath(
path.join(projectDir, "src/app/(main)/layout.tsx")
)
);

await formatAndSaveSourceFiles(project);

const hasProofKitLayouts = await checkForProofKitLayouts(projectDir);
Expand Down Expand Up @@ -144,6 +152,27 @@ function addToSafeActionClient(sourceFile?: SourceFile) {
);
}

function protectMainLayout(sourceFile: SourceFile) {
sourceFile.addImportDeclaration({
defaultImport: "Protect",
moduleSpecifier: "@/components/auth/protect",
});

// inject query provider into the root layout

const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport());
const bodyElement = exportDefault
?.getBody()
?.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
?.getFirstDescendantByKind(SyntaxKind.JsxElement);

bodyElement?.replaceWithText(
`<Protect>
${bodyElement?.getText()}
</Protect>`
);
}

async function checkForProofKitLayouts(projectDir: string): Promise<boolean> {
const settings = getSettings();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function UpdatePasswordForm() {
label="Password"
style={{ flexGrow: 1 }}
value="••••••••"
readOnly
/>
<Button
size="sm"
Expand Down
4 changes: 3 additions & 1 deletion cli/template/extras/proofkit-auth/app/auth/login/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
setSessionTokenCookie,
} from "@/server/auth/utils/session";
import { redirect } from "next/navigation";
import { getRedirectCookie } from "@/server/auth/utils/redirect";

export const loginAction = actionClient
.schema(loginSchema)
Expand All @@ -28,5 +29,6 @@ export const loginAction = actionClient
return redirect("/auth/verify-email");
}

return redirect("/");
const redirectTo = await getRedirectCookie();
return redirect(redirectTo);
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { invalidateUserPasswordResetSessions } from "@/server/auth/utils/password-reset";
import { updateUserEmailAndSetEmailAsVerified } from "@/server/auth/utils/user";
import { redirect } from "next/navigation";
import { getRedirectCookie } from "@/server/auth/utils/redirect";

export const verifyEmailAction = actionClient
.schema(emailVerificationSchema)
Expand Down Expand Up @@ -67,7 +68,9 @@ export const verifyEmailAction = actionClient
verificationRequest.email
);
await deleteEmailVerificationRequestCookie();
return redirect("/");

const redirectTo = await getRedirectCookie();
return redirect(redirectTo);
});

export const resendEmailVerificationAction = actionClient.action(async () => {
Expand Down
11 changes: 4 additions & 7 deletions cli/template/extras/proofkit-auth/app/auth/verify-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { Anchor, Container, Text, Title } from "@mantine/core";
import { redirect } from "next/navigation";
import EmailVerificationForm from "./email-verification-form";
import ResendButton from "./resend-button";
import {
createEmailVerificationRequest,
getUserEmailVerificationRequestFromRequest,
sendVerificationEmail,
setEmailVerificationRequestCookie,
} from "@/server/auth/utils/email-verification";
import { getUserEmailVerificationRequestFromRequest } from "@/server/auth/utils/email-verification";
import { getRedirectCookie } from "@/server/auth/utils/redirect";

export default async function Page() {
const { user } = await getCurrentSession();
Expand All @@ -21,7 +17,8 @@ export default async function Page() {
// but we can't set cookies inside server components.
let verificationRequest = await getUserEmailVerificationRequestFromRequest();
if (verificationRequest === null && user.emailVerified) {
return redirect("/");
const redirectTo = await getRedirectCookie();
return redirect(redirectTo);
}

return (
Expand Down
17 changes: 17 additions & 0 deletions cli/template/extras/proofkit-auth/components/auth/protect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getCurrentSession } from "@/server/auth/utils/session";
import AuthRedirect from "./redirect";

/**
* This server component will protect the contents of it's children from users who aren't logged in
* It will redirect to the login page if the user is not logged in, or the verify email page if the user is logged in but hasn't verified their email
*/
export default async function Protect({
children,
}: {
children: React.ReactNode;
}) {
const { session, user } = await getCurrentSession();
if (!session) return <AuthRedirect path="/auth/login" />;
if (!user.emailVerified) return <AuthRedirect path="/auth/verify-email" />;
return <>{children}</>;
}
26 changes: 26 additions & 0 deletions cli/template/extras/proofkit-auth/components/auth/redirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { redirect } from "next/navigation";
import { Center, Loader } from "@mantine/core";
import { useEffect } from "react";
import Cookies from "js-cookie";

/**
* A client-side component that redirects to the given path, but saves the current path in the redirectTo cookie.
*/
export default function AuthRedirect({ path }: { path: string }) {
useEffect(() => {
if (typeof window !== "undefined") {
Cookies.set("redirectTo", window.location.pathname, {
expires: 1 / 24 / 60, // 1 hour
});
redirect(path);
}
}, []);

return (
<Center h="100vh">
<Loader />
</Center>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export async function invalidateUserPasswordResetSessions(
): Promise<void> {
const sessions = await passwordResetLayout.find({
query: { id_user: `==${userId}` },
ignoreEmptyResult: true,
});
for (const session of sessions.data) {
await passwordResetLayout.delete({ recordId: session.recordId });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { cookies } from "next/headers";

export async function getRedirectCookie() {
const cookieStore = await cookies();
const redirectTo = cookieStore.get("redirectTo")?.value;
cookieStore.delete("redirectTo");
return redirectTo ?? "/";
}

0 comments on commit c2b4b06

Please sign in to comment.