Skip to content

Commit

Permalink
Start of sessions & authentication work
Browse files Browse the repository at this point in the history
  • Loading branch information
chadmmills committed Nov 28, 2023
1 parent 6b8f78b commit 722b6ac
Show file tree
Hide file tree
Showing 26 changed files with 1,344 additions and 43 deletions.
2 changes: 1 addition & 1 deletion packages/api/orm/gen-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,5 @@ export async function call(_: any, db: any, config?: Config) {

fileConentLines.push("export type ORM = typeof orm");

await writeToFile("api/orm/gen.ts", fileConentLines.join("\n"));
await writeToFile("orm/gen.ts", fileConentLines.join("\n"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function up(): string {
return `
CREATE TABLE auth_sessions (
id text PRIMARY KEY NOT NULL,
user_id text NOT NULL,
expires_at timestamp NOT NULL,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);`
}

export function down(): string | void {
return 'DROP TABLE auth_sessions'
}
2 changes: 1 addition & 1 deletion packages/api/orm/migrations/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function call(
const timestamp = Date.now();
const uuid = config.getID();
const fileName = `${timestamp}_${uuid}_${inputs[0]}.ts`;
const path = `api/orm/migrations/files/${fileName}`;
const path = `orm/migrations/files/${fileName}`;

config.logger(` '${fileName}'`);

Expand Down
2 changes: 1 addition & 1 deletion packages/api/orm/migrations/migrate-down.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type Config = {
};

function getFiles(): string[] {
return getFilesFromDirectory("api/orm/migrations/files");
return getFilesFromDirectory("orm/migrations/files");
}

async function importer(path: string): Promise<{ down?: () => string }> {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/orm/migrations/migrate-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type Config = {
};

function getFiles(): string[] {
return getFilesFromDirectory("api/orm/migrations/files");
return getFilesFromDirectory("orm/migrations/files");
}

type DB = {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/orm/migrations/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Config = {
};

function getFiles(): string[] {
return getFilesFromDirectory("api/orm/migrations/files");
return getFilesFromDirectory("orm/migrations/files");
}

async function importer(path: string) {
Expand Down
16 changes: 8 additions & 8 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "api",
"scripts": {
"db:setup": "bun run api/orm/cli.ts -- --task=setup",
"db:reset": "bun run api/orm/cli.ts -- --task=reset",
"db:setup": "bun run orm/cli.ts -- --task=setup",
"db:reset": "bun run orm/cli.ts -- --task=reset",
"db:rebuild": "bun db:reset && bun db:migrate",
"db:migrate": "bun run api/orm/cli.ts -- --task=migrate",
"db:migration:down": "bun run api/orm/cli.ts -- --task=migrateDown",
"db:migrate:list": "bun run api/orm/cli.ts -- --task=migrateList",
"db:gen:migration": "bun run api/orm/cli.ts -- --task=generate",
"db:gen:types": "bun run api/orm/cli.ts -- --task=generateTypes",
"db:schema": "bun run api/orm/cli.ts -- --task=schema",
"db:migrate": "bun run orm/cli.ts -- --task=migrate",
"db:migration:down": "bun run orm/cli.ts -- --task=migrateDown",
"db:migrate:list": "bun run orm/cli.ts -- --task=migrateList",
"db:gen:migration": "bun run orm/cli.ts -- --task=generate",
"db:gen:types": "bun run orm/cli.ts -- --task=generateTypes",
"db:schema": "bun run orm/cli.ts -- --task=schema",
"fmt:check": "prettier --check ."
},
"dependencies": {
Expand Down
27 changes: 27 additions & 0 deletions packages/api/routes/api/sessions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from "zod";

import type { ApiHandlerArgs, ApiHandlerResponse } from "api/index.ts";

export function post({ payload }: ApiHandlerArgs): ApiHandlerResponse {
let body = payloadParser(payload);
let session = AuthSession.create(body);

if (session.ok) {
throw new Response("", { status: 404 });
}

return new Response(JSON.stringify({}), {
headers: { "content-type": "application/json" },
status: 400,
})
}

const sessionPayloadSchema = z.object({
email: z.string().email(),
password: z.string().min(3).max(255),
});

// used to parse the body of a request
export const payloadParser = (raw: unknown, parser: typeof sessionPayloadSchema = sessionPayloadSchema) => {
return parser.parse(raw);
};
22 changes: 21 additions & 1 deletion packages/api/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,25 @@ const fetch = makeFetch(router, orm);

export default {
port: 3008,
fetch,
fetch: middlewares(fetch, logRequest),
} satisfies Serve;


type MiddlewareFn = (req: Request, res: Response) => Promise<Response>;

function middlewares(...fns: MiddlewareFn[]) {
return async function (req: Request): Promise<Response> {
let current: Response = new Response(null, { status: 404 });
for (let fn of fns) {
current = await fn(req, current);
}

return current;
};
}

function logRequest(req: Request, res: Response): Promise<Response> {
console.info(req.method + ": " + new URL(req.url).pathname + " " + res.status);

return Promise.resolve(res);
}
1 change: 0 additions & 1 deletion packages/api/server/make-fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export function makeFetch<ORM>(
config: Config = { getParams: getParamsFromPath, makeResponse },
) {
return async (req: Request) => {
console.info(req.method + ": " + new URL(req.url).pathname);
const maybeRoute = router.find(req);
if (maybeRoute) {
// handler({ req, db, json() {}, setHeader() {}, setStatus() {} }) // or something
Expand Down
6 changes: 5 additions & 1 deletion packages/web/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node", "prettier"],
plugins: ["prettier"],
rules: {
"prettier/prettier": "error"
}
};
12 changes: 12 additions & 0 deletions packages/web/app/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Button as RAButton, ButtonProps } from "react-aria-components";

export default function Button({ children, className }: ButtonProps) {
return (
<RAButton
type="submit"
className={`px-2 py-1 border border-gray-200 rounded-md ${className || ""}`}
>
{children}
</RAButton>
);
}
36 changes: 26 additions & 10 deletions packages/web/app/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import {TextField, Label, Input as RAInput} from 'react-aria-components';
import { TextField, Label, Input as RAInput } from "react-aria-components";

export default function InputComponent({label, name, type, value, onChange}: InputProps) {
type InputProps = {
label: string;
name: string;
type: "email" | "password" | "text";
value?: string | number;
};

export default function InputComponent({
label,
name,
type,
value,
}: InputProps) {
return (
<TextField>
<Label>Email</Label>
<RAInput type='email'
className="px-2 py-1 border border-gray-200 rounded-md"
/>
</TextField>
)
<TextField>
<Label>
{label}
<RAInput
type={type}
name={name}
className="block px-2 py-1 border border-gray-200 rounded-md"
value={value}
/>
</Label>
</TextField>
);
}

29 changes: 29 additions & 0 deletions packages/web/app/main/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type AuthSession from "../auth-session";

const Api = {
postSession: async (session: AuthSession) => {
try {
let resp = await fetch("http://localhost:3008/api/sessions", {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(session.attributes),
});

if (resp.ok) {
let data = await resp.json(); // ?.token;
if (data) {
return { isError: false, token: data.token };
}
}
return { isError: true, errors: { email: ["is required"] } };
} catch (e) {
console.log(e);
return { isError: true, errors: { networkError: e } };
}
},
};

export default Api;
40 changes: 40 additions & 0 deletions packages/web/app/main/auth-session/auth-session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Request, FormData } from "@remix-run/node";
import { expect, test, describe } from "vitest";

import AuthSession from ".";

describe("AuthSession", () => {
test("Should show errors when no email is provided", async () => {
let req = new Request("http://example.com", {
method: "POST",
body: new FormData(),
});

let session = new AuthSession(req);
await session.create();

expect(session.isValid).toBeFalsy();
expect(session.errors.email).toEqual(["is required"]);
});

test("it should show errors from requestJWTForSession", async () => {
let fd = new FormData();
fd.set("email", "hey");
fd.set("password", "password");

let jwtRequest = async () => {
return { isError: true, errors: { "*": ["Email/pw combo is invalid"] } };
};

let req = new Request("http://example.com", {
method: "POST",
body: fd,
});

let session = new AuthSession(req, { requestJWTForSession: jwtRequest });
await session.create();

expect(session.isValid).toBeFalsy();
expect(session.errors["*"]).toEqual(["Email/pw combo is invalid"]);
});
});
72 changes: 72 additions & 0 deletions packages/web/app/main/auth-session/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ValidationErrors } from "../validate";
import validate from "../validate";
import Api from "../api";

type AuthSessionOptions = {
validateData?: typeof validate;
requestJWTForSession?: (session: AuthSession) => Promise<{
isError: boolean;
errors?: any;
token?: string;
}>;
};

export default class AuthSession {
private request: Request;
private requestJWTForSession: (
session: AuthSession,
) => Promise<{ isError: boolean; errors?: any; token?: string }>;

attributes: { email: string; password: string };
errors: ValidationErrors = {};
token: string = "";
validateData: typeof validate;

constructor(req: Request, options?: AuthSessionOptions) {
const { validateData = validate, requestJWTForSession = Api.postSession } =
options || {};

this.attributes = { email: "", password: "" };
this.request = req;
this.validateData = validateData;
this.requestJWTForSession = requestJWTForSession;
}

static init(req: Request) {
return new AuthSession(req);
}

async create() {
let formData = await this.request.formData();
this.attributes = {
email: String(formData.get("email")),
password: String(formData.get("password")),
};

let validationResult = this.validateData(
{ email: true, password: true },
formData,
);

if (validationResult.isInValid) {
this.errors = validationResult.errors;
return this;
}

let tokenResult = await this.requestJWTForSession(this);
if (tokenResult.isError) {
this.errors = tokenResult.errors;
return this;
}

if (tokenResult.token) {
this.token = tokenResult.token;
}

return this;
}

get isValid() {
return Object.keys(this.errors).length === 0;
}
}
14 changes: 14 additions & 0 deletions packages/web/app/main/auth-session/session-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { expect, test, describe } from "vitest";

import { create } from "./session-token";

describe("create session token", () => {
test("should return a jwt if email and password are valid", async () => {
expect(
create({
email: "ok",
password: "ok",
})
).toEqual("jwt");
});
});
6 changes: 6 additions & 0 deletions packages/web/app/main/auth-session/session-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type UserFields = {
email: string;
password: string;
};

// export function create(
10 changes: 10 additions & 0 deletions packages/web/app/main/form-data-from-request/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default async function formDataFromRequest(req: Request) {
const formData = await req.clone().formData();
const path = new URL(req.url).pathname;

return (
path +
"\n" +
[...formData.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n")
);
}
7 changes: 7 additions & 0 deletions packages/web/app/main/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default class Logger {
static debug(...args: any[]) {
if (process.env.NODE_ENV !== "production") {
console.debug(...args);
}
}
}
Loading

0 comments on commit 722b6ac

Please sign in to comment.