Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
Octo8080X committed Dec 30, 2023
1 parent dbc1937 commit 386a0c8
Show file tree
Hide file tree
Showing 19 changed files with 920 additions and 0 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Test

on:
push:
branches: ["main", "test"]
pull_request:
branches: ["main", "test"]
schedule:
# Run every Monday at 00:00 UTC
- cron: '0 0 * * 1'

jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Setup repo
uses: actions/checkout@v3

- name: Setup Deno
uses: denoland/setup-deno@v1

- name: Check version
run: deno -V

- name: Verify formatting
run: deno fmt --check

- name: Install Fresh
run: deno run -A https://deno.land/x/fresh/init.ts ./tests/work --force --twind --tailwind --vscode

- name: Move deno.json
run: mv ./tests/work/deno.json ./deno.json

- name: view deno.json
run: cat ./deno.json

- name: Run tests
run: deno test --unstable --allow-read --allow-write --allow-env --allow-net --no-check --coverage=./coverage

- name: View coverage
run: |
# reference: https://github.com/jhechtf/code-coverage
deno install --allow-read --no-check -n code-coverage https://deno.land/x/code_coverage/cli.ts &&
deno coverage --exclude=tests/work/ --lcov --output=lcov.info ./coverage/ &&
code-coverage --file lcov.info
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
deno.json
tests/work/
node_modules/
Binary file modified README.md
Binary file not shown.
11 changes: 11 additions & 0 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { parseArgs } from "https://deno.land/[email protected]/cli/parse_args.ts";
import { ensureDirSync } from "https://deno.land/[email protected]/fs/ensure_dir.ts";

const parsedArgs = parseArgs(Deno.args);

ensureDirSync(`./plantation`);
ensureDirSync(`./plantation/${parsedArgs["_"][0]}`);

Deno.writeTextFileSync(`./plantation/${parsedArgs["_"][0]}/create.tsx`, "AAA")
Deno.writeTextFileSync(`./plantation/${parsedArgs["_"][0]}/login.tsx`, "AAA")
Deno.writeTextFileSync(`./plantation/${parsedArgs["_"][0]}/logout.tsx`, "AAA")
20 changes: 20 additions & 0 deletions components/LogoutForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { JSX } from "preact";

interface LogoutFormProps extends JSX.HTMLAttributes<HTMLButtonElement> {
csrfToken: string;
actionPath: string;
}

export function LogoutForm(
{ csrfToken, actionPath, ...other }: LogoutFormProps,
) {
return (
<form action={actionPath} method="post">
<input type="hidden" name="csrf" value={csrfToken} />
<button
type="submit"
{...other}
/>
</form>
);
}
20 changes: 20 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type {
AppProps,
FreshContext,
Handler,
HandlerContext,
Handlers,
MiddlewareHandlerContext,
PageProps,
Plugin,
PluginRoute,
} from "https://deno.land/x/[email protected]/server.ts";
export type { ComponentType } from "https://esm.sh/[email protected]";
export type { Auth, Session } from "npm:lucia";
export { LuciaError } from "npm:lucia";
export { z, type ZodIssue } from "https://deno.land/x/[email protected]/mod.ts";
export { fromFileUrl } from "https://deno.land/[email protected]/path/mod.ts";
export { existsSync } from "https://deno.land/[email protected]/fs/exists.ts";
export { pascalCase } from "https://deno.land/x/case/mod.ts";
export { type WithCsrf, type CsrfOption, getCsrfPlugin} from "https://deno.land/x/[email protected]/mod.ts";
export { type JSX, h } from "https://esm.sh/[email protected]";
44 changes: 44 additions & 0 deletions middlewares/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { FreshContext } from "../deps.ts";
import type { PlantationInnerParams } from "../types.ts";

export function getPlantationMiddleware({
auth,
paths,
resourceName,
loginAfterPath,
isAllowNoSessionPath,
isSessionLogicPath,
}: PlantationInnerParams) {
return async function (req: Request, ctx: FreshContext) {
if (ctx.destination !== "route") {
return await ctx.next();
}

const pathname = new URL(req.url).pathname;
const authRequest = auth.handleRequest(req);
const session = await authRequest.validate();

if (isSessionLogicPath(pathname)) {
if (session) {
return new Response("Authorized", {
status: 302,
headers: { Location: loginAfterPath },
});
} else {
return await ctx.next();
}
} else {
if (!isAllowNoSessionPath(pathname) && !session) {
return new Response("Unauthorized", {
status: 302,
headers: { Location: paths.loginPath },
});
}
}

if (session) {
ctx.state[`auth${resourceName}Session`] = session;
}
return await ctx.next();
};
}
10 changes: 10 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export {
getPlantationPlugin,
getPlantationWithCsrfPlugins,
} from "./plugins/plugin.tsx";
export {
emailSchema,
passwordSchema,
usernameSchema,
} from "./utils/validates.ts";
export type { PlantationParams } from "./types.ts";
62 changes: 62 additions & 0 deletions plugins/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { type Plugin, type CsrfOption, getCsrfPlugin } from "../deps.ts";
import { DefaultActions, PlantationParams } from "../types.ts";
import { getCreateComponent, getCreateHandler } from "../routes/create.tsx";
import { getLoginComponent, getLoginHandler } from "../routes/login.tsx";
import { getLogoutHandler } from "../routes/logout.tsx";

import { getPlantationMiddleware } from "../middlewares/middleware.ts";
import { getInnerParams } from "../utils/params.ts";
import {
getCreateRoute,
getLoginRoute,
getLogoutRoute,
} from "../utils/route.ts";

const defaultActions: DefaultActions = {
create: {
getHandler: getCreateHandler,
getComponent: getCreateComponent,
},
login: {
getHandler: getLoginHandler,
getComponent: getLoginComponent,
},
logout: {
getHandler: getLogoutHandler,
},
};

export async function getPlantationPlugin(
plantationParams: PlantationParams,
): Promise<Plugin> {
const plantationInnerParams = getInnerParams(plantationParams);

return {
name: "plantation",
middlewares: [
{
middleware: {
handler: getPlantationMiddleware(plantationInnerParams),
},
path: plantationParams.setupRootPath,
},
],
routes: [
await getCreateRoute(plantationInnerParams, defaultActions),
await getLoginRoute(plantationInnerParams, defaultActions),
await getLogoutRoute(plantationInnerParams, defaultActions),
],
};
}

export async function getPlantationWithCsrfPlugins(
{ csrf, plantationParams }: {
csrf: { kv: Deno.Kv; csrfOption?: Partial<CsrfOption> | undefined };
plantationParams: PlantationParams;
},
) {
return [
await getCsrfPlugin(csrf.kv, csrf.csrfOption),
await getPlantationPlugin(plantationParams),
];
}
148 changes: 148 additions & 0 deletions routes/create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/** @jsx h */
import { h, FreshContext, LuciaError, PageProps, pascalCase, WithCsrf } from "../deps.ts";
import { PlantationInnerParams } from "../types.ts";
import { styles } from "../utils/style.ts";
import { sameLogicValidate } from "../utils/validates.ts";
import { PASSWORD } from "../utils/const.ts";

export function getCreateHandler(
{
auth,
loginAfterPath,
resourceIdentifierName,
identifierSchema,
passwordSchema,
}: PlantationInnerParams,
) {
return {
async POST(req: Request, ctx: FreshContext<WithCsrf>) {
const formData = await req.formData();
const token = formData.get("csrf");

if (!ctx.state.csrf.csrfVerifyFunction(token?.toString() ?? null)) {
return new Response(null, {
status: 302,
headers: {
Location: req.headers.get("referer") || "/",
},
});
}

const identifier = formData.get(resourceIdentifierName);
const password = formData.get(PASSWORD);

const identifierResult = sameLogicValidate(
resourceIdentifierName,
identifier?.toString(),
identifierSchema,
);
const passwordResult = sameLogicValidate(
"password",
password?.toString(),
passwordSchema,
);

if (!(identifierResult.success && passwordResult.success)) {
return ctx.render({
errors: [...identifierResult.errors, ...passwordResult.errors],
identifier,
});
}

try {
const user = await auth.createUser({
key: {
providerId: resourceIdentifierName,
providerUserId: identifierResult.data,
password: passwordResult.data,
},
attributes: {
[resourceIdentifierName]: identifierResult.data,
},
});
const session = await auth.createSession({
userId: user.userId,
attributes: {},
});
const sessionCookie = auth.createSessionCookie(session);

return new Response(null, {
headers: {
Location: loginAfterPath,
"Set-Cookie": sessionCookie.serialize(), // store session cookie
},
status: 302,
});
} catch (e) {
console.error("e", e);
if (e instanceof LuciaError) {
return ctx.render({
errors: ["Auth system error"],
identifier,
});
}
return new Response("An unknown error occurred", {
status: 500,
});
}
},
};
}

export function getCreateComponent(
{ resourceIdentifierName, paths }: PlantationInnerParams,
) {
return function (
{ data, state }: PageProps<
{ errors: string[]; identifier: string },
WithCsrf
>,
) {
return (
<div style={styles.block}>
<div>
<h2>Create Account</h2>
</div>
<div>
<form action={paths.createPath} method="post">
<input type="hidden" name="csrf" value={state.csrf.getTokenStr()} />
<div style={styles.row}>
{data?.errors?.length > 0 && (
<ul>
{data.errors.map((error) => <li>{error}</li>)}
</ul>
)}
</div>
<div style={styles.row}>
<label for={resourceIdentifierName} style={styles.label}>
{pascalCase(resourceIdentifierName)}
</label>
<input
type="text"
id={resourceIdentifierName}
name={resourceIdentifierName}
style={styles.textbox}
value={data?.identifier}
/>
</div>
<div style={styles.row}>
<label for="password" style={styles.label}>Password</label>
<input
type="password"
id={PASSWORD}
name={PASSWORD}
style={styles.textbox}
/>
</div>
<div style={styles.row}>
<button type="submit" style={styles.button}>CREATE</button>
</div>
</form>
</div>
<div>
<a href={paths.loginPath} style={styles.link}>LOGIN</a>
</div>
</div>
);
};
}
Loading

0 comments on commit 386a0c8

Please sign in to comment.