Super lazy registration
This commit is contained in:
parent
e534f234d1
commit
aed2cdfa0c
|
@ -1,32 +1,25 @@
|
||||||
import { JSX } from "preact";
|
import { JSX } from "preact";
|
||||||
|
|
||||||
const NAV_ITEMS = {
|
const NAV_ITEM_CLASSES =
|
||||||
"/note": "Notes",
|
"flex justify-center items-center px-4 py-2 text-blue-500 hover:bg-purple-700";
|
||||||
"/register": "Register",
|
|
||||||
"/login": "Login",
|
|
||||||
};
|
|
||||||
|
|
||||||
const navItem = ([url, text]: [string, string]) => {
|
|
||||||
return (
|
|
||||||
<a class="px-4 py-2 text-blue-500 hover:bg-gray-900" href={url}>{text}</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Page(props: JSX.HTMLAttributes<HTMLDivElement>) {
|
export function Page(props: JSX.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="relative min-h-screen flex flex-col">
|
||||||
<header>
|
<header class="flex justify-start items-center">
|
||||||
<h1>
|
<nav class="flex bg-gray-800 w-full drop-shadow-md">
|
||||||
<a href="/">LyricScreen</a>
|
<a href="/" class={NAV_ITEM_CLASSES}>
|
||||||
</h1>
|
<h1 class="text-2xl">LyricScreen</h1>
|
||||||
<nav class="flex">
|
</a>
|
||||||
{Object.entries(NAV_ITEMS).map(navItem)}
|
<a href="/note" class={NAV_ITEM_CLASSES}>Notes</a>
|
||||||
|
<a href="/register" class={NAV_ITEM_CLASSES}>Register</a>
|
||||||
|
<a href="/login" class={NAV_ITEM_CLASSES}>Login</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main class="p-2">
|
||||||
{props.children}
|
{props.children}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer class="p-2 bg-gray-800 w-full mt-auto">
|
||||||
"It's a bit much, really..."
|
"It's a bit much, really..."
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,39 @@
|
||||||
import { query } from "./db.ts";
|
import { query } from "./db.ts";
|
||||||
|
|
||||||
await query(`
|
const id = "id uuid primary key default uuid_generate_v4()";
|
||||||
create extension if not exists "uuid-ossp";
|
|
||||||
|
|
||||||
drop table if exists notes;
|
const timestamps = [
|
||||||
create table if not exists notes (
|
"created_at timestamptz not null default now()",
|
||||||
id uuid primary key default uuid_generate_v4(),
|
"updated_at timestamptz not null default now()",
|
||||||
content text not null,
|
];
|
||||||
created_at timestamptz not null default now(),
|
|
||||||
updated_at timestamptz not null default now()
|
const tables = {
|
||||||
|
"note": {
|
||||||
|
columns: [id, "content text not null", ...timestamps],
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
columns: [
|
||||||
|
id,
|
||||||
|
"username text not null unique",
|
||||||
|
"hashed_password text not null",
|
||||||
|
...timestamps,
|
||||||
|
],
|
||||||
|
constraints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableStatements = Object.entries(tables).map(([name, meta]) => `
|
||||||
|
drop table if exists "${name}";
|
||||||
|
create table "${name}" (
|
||||||
|
${meta.columns.join(",\n ")}
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
console.log(tableStatements);
|
||||||
|
|
||||||
|
const queryString = `
|
||||||
|
create extension if not exists "uuid-ossp";
|
||||||
|
${tableStatements.map((s) => s.trim()).join("\n\n ")}
|
||||||
|
`;
|
||||||
|
console.log(queryString);
|
||||||
|
await query(queryString);
|
||||||
|
|
22
db.ts
22
db.ts
|
@ -1,18 +1,28 @@
|
||||||
import * as postgres from "https://deno.land/x/postgres@v0.16.1/mod.ts";
|
import {
|
||||||
import { type QueryArguments } from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments";
|
Pool,
|
||||||
|
PostgresError,
|
||||||
|
} from "https://deno.land/x/postgres@v0.16.1/mod.ts";
|
||||||
|
import {
|
||||||
|
type QueryArguments,
|
||||||
|
type QueryObjectResult,
|
||||||
|
} from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments";
|
||||||
|
|
||||||
export { type QueryObjectResult } from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments";
|
export { PostgresError };
|
||||||
|
export { type QueryObjectResult };
|
||||||
|
|
||||||
const databaseUrl = Deno.env.get("DATABASE_URL") ||
|
const databaseUrl = Deno.env.get("DATABASE_URL") ||
|
||||||
"postgresql://danielflanagan:@127.0.0.1:5432/lyricscreen";
|
"postgresql://danielflanagan:@127.0.0.1:5432/lyricscreen";
|
||||||
const pool = new postgres.Pool(databaseUrl, 3, true);
|
const pool = new Pool(databaseUrl, 3, true);
|
||||||
|
|
||||||
export async function query(sql: string, ...args: QueryArguments[]) {
|
export async function query<T>(
|
||||||
|
sql: string,
|
||||||
|
...args: QueryArguments[]
|
||||||
|
): Promise<QueryObjectResult<T> | null> {
|
||||||
let result = null;
|
let result = null;
|
||||||
try {
|
try {
|
||||||
const connection = await pool.connect();
|
const connection = await pool.connect();
|
||||||
try {
|
try {
|
||||||
result = await connection.queryObject(sql, ...args);
|
result = connection.queryObject<T>(sql, ...args);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error querying database:", err);
|
console.error("Error querying database:", err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
22
fresh.gen.ts
22
fresh.gen.ts
|
@ -13,10 +13,13 @@ import * as $6 from "./routes/api/random-uuid.ts";
|
||||||
import * as $7 from "./routes/countdown.tsx";
|
import * as $7 from "./routes/countdown.tsx";
|
||||||
import * as $8 from "./routes/github/[username].tsx";
|
import * as $8 from "./routes/github/[username].tsx";
|
||||||
import * as $9 from "./routes/index.tsx";
|
import * as $9 from "./routes/index.tsx";
|
||||||
import * as $10 from "./routes/note.tsx";
|
import * as $10 from "./routes/login.tsx";
|
||||||
import * as $11 from "./routes/register.tsx";
|
import * as $11 from "./routes/note.tsx";
|
||||||
import * as $12 from "./routes/route-config-example.tsx";
|
import * as $12 from "./routes/note/[id].tsx";
|
||||||
import * as $13 from "./routes/search.tsx";
|
import * as $13 from "./routes/note/create.tsx";
|
||||||
|
import * as $14 from "./routes/register.tsx";
|
||||||
|
import * as $15 from "./routes/route-config-example.tsx";
|
||||||
|
import * as $16 from "./routes/search.tsx";
|
||||||
import * as $$0 from "./islands/Countdown.tsx";
|
import * as $$0 from "./islands/Countdown.tsx";
|
||||||
import * as $$1 from "./islands/Counter.tsx";
|
import * as $$1 from "./islands/Counter.tsx";
|
||||||
|
|
||||||
|
@ -32,10 +35,13 @@ const manifest = {
|
||||||
"./routes/countdown.tsx": $7,
|
"./routes/countdown.tsx": $7,
|
||||||
"./routes/github/[username].tsx": $8,
|
"./routes/github/[username].tsx": $8,
|
||||||
"./routes/index.tsx": $9,
|
"./routes/index.tsx": $9,
|
||||||
"./routes/note.tsx": $10,
|
"./routes/login.tsx": $10,
|
||||||
"./routes/register.tsx": $11,
|
"./routes/note.tsx": $11,
|
||||||
"./routes/route-config-example.tsx": $12,
|
"./routes/note/[id].tsx": $12,
|
||||||
"./routes/search.tsx": $13,
|
"./routes/note/create.tsx": $13,
|
||||||
|
"./routes/register.tsx": $14,
|
||||||
|
"./routes/route-config-example.tsx": $15,
|
||||||
|
"./routes/search.tsx": $16,
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
"./islands/Countdown.tsx": $$0,
|
"./islands/Countdown.tsx": $$0,
|
||||||
|
|
15
routes/login.tsx
Normal file
15
routes/login.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Page } from "../components/Page.tsx";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<form class="flex flex-col max-w-lg" method="post">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" name="username" />
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" />
|
||||||
|
<input class="mt-2" type="submit" value="Login" />
|
||||||
|
</form>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { query } from "../db.ts";
|
import { query } from "../db.ts";
|
||||||
|
import { Page } from "../components/Page.tsx";
|
||||||
|
|
||||||
interface Note {
|
interface Note {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -7,10 +8,10 @@ interface Note {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handler: Handlers<Note[] | null> = {
|
export const handler: Handlers<Note[]> = {
|
||||||
async GET(request, context) {
|
async GET(request, context) {
|
||||||
console.debug({ request, context });
|
console.debug({ request, context });
|
||||||
const result = await query("select * from notes");
|
const result = await query("select * from note order by created_at desc");
|
||||||
if (result == null) throw "unable to fetch from database";
|
if (result == null) throw "unable to fetch from database";
|
||||||
const notes = result.rows as Note[];
|
const notes = result.rows as Note[];
|
||||||
console.debug(notes);
|
console.debug(notes);
|
||||||
|
@ -18,21 +19,27 @@ export const handler: Handlers<Note[] | null> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Page({ data }: PageProps<Note[] | null>) {
|
export default function NotesPage({ data: notes }: PageProps<Note[]>) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Page>
|
||||||
<h1>List of Notes</h1>
|
<h1>List of Notes</h1>
|
||||||
<nav>
|
|
||||||
<a href="/" class="text-blue-500 p-2">Back to Index</a>
|
|
||||||
</nav>
|
|
||||||
<p>Create a note:</p>
|
<p>Create a note:</p>
|
||||||
<form action="./create" method="POST">
|
<form class="flex flex-col" action="./note/create" method="POST">
|
||||||
<textarea name="content"></textarea>
|
<textarea rows="6" class="px-4 py-2 bg-gray-800" name="content">
|
||||||
<button>Post</button>
|
</textarea>
|
||||||
|
<button class="mt-2 px-4 py-2 bg-gray-800">Post</button>
|
||||||
</form>
|
</form>
|
||||||
<pre>
|
{notes.map(({ id, content, created_at }) => (
|
||||||
{JSON.stringify(data, null, 2)}
|
<div class="my-4" key={id}>
|
||||||
</pre>
|
<span>
|
||||||
|
<a href={`/note/${id}`} class="text-blue-500">Note {id}</a>{" "}
|
||||||
|
created at {created_at.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<blockquote class="mt-2 pl-4 border-l(solid 4)">
|
||||||
|
<pre>{content}</pre>
|
||||||
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
37
routes/note/[id].tsx
Normal file
37
routes/note/[id].tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
import { query } from "../../db.ts";
|
||||||
|
import { Page } from "../../components/Page.tsx";
|
||||||
|
|
||||||
|
interface Note {
|
||||||
|
id: string;
|
||||||
|
created_at: Date;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<Note> = {
|
||||||
|
async GET(request, context) {
|
||||||
|
console.debug({ request, context });
|
||||||
|
const result = await query(
|
||||||
|
"select * from note where id = $1 order by created_at desc",
|
||||||
|
[context.params["id"]],
|
||||||
|
);
|
||||||
|
if (result == null) throw "unable to fetch from database";
|
||||||
|
const [note] = result.rows as Note[];
|
||||||
|
return await context.render(note);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotesPage(
|
||||||
|
{ data: { id, created_at, content } }: PageProps<Note>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<h1>Note {id} created at {created_at.toLocaleString()}</h1>
|
||||||
|
<div class="my-4" key={id}>
|
||||||
|
<blockquote class="mt-2 pl-4 border-l(solid 4)">
|
||||||
|
<pre>{content}</pre>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
29
routes/note/create.tsx
Normal file
29
routes/note/create.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
import { query } from "../../db.ts";
|
||||||
|
import { Page } from "../../components/Page.tsx";
|
||||||
|
|
||||||
|
type NoteID = string;
|
||||||
|
|
||||||
|
export const handler: Handlers<NoteID> = {
|
||||||
|
async POST(request, context) {
|
||||||
|
const content = (await request.formData()).get("content");
|
||||||
|
if (!content) throw "no content provided";
|
||||||
|
const result = await query(
|
||||||
|
"insert into note (content) values ($1) returning id",
|
||||||
|
[content],
|
||||||
|
);
|
||||||
|
if (!result) throw "insert failed";
|
||||||
|
const { rows: [{ id }] } = result;
|
||||||
|
return await context.render(id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotesPage({ data: noteId }: PageProps<NoteID>) {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<h1>You created a note!</h1>
|
||||||
|
<a href="/note">Back to notes</a>
|
||||||
|
<a href={`/note/${noteId}`}>View your note</a>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,11 +1,79 @@
|
||||||
export default function Register() {
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
import { Page } from "../components/Page.tsx";
|
||||||
|
import { PostgresError, query } from "../db.ts";
|
||||||
|
import { hash } from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
|
||||||
|
|
||||||
|
type UserID = string;
|
||||||
|
|
||||||
|
interface RegistrationError {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<UserID | RegistrationError | null> = {
|
||||||
|
async POST(request, context) {
|
||||||
|
const formData = (await request.formData());
|
||||||
|
const username = formData.get("username");
|
||||||
|
const password = formData.get("password");
|
||||||
|
if (!username) throw "no username provided";
|
||||||
|
if (!password) throw "no password provided";
|
||||||
|
const hashed_password = await hash(password.toString());
|
||||||
|
try {
|
||||||
|
const result = await query<{ id: string }>(
|
||||||
|
`insert into "user" (username, hashed_password) values ($1, $2) returning id`,
|
||||||
|
[username, hashed_password],
|
||||||
|
);
|
||||||
|
console.debug(result);
|
||||||
|
if (!result) throw "insert failed";
|
||||||
|
const { rows: [{ id }] } = result;
|
||||||
|
return await context.render(id);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof PostgresError) {
|
||||||
|
console.error("PostgresError:", err);
|
||||||
|
return await context.render({ message: err.message });
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Register(
|
||||||
|
{ data: userId }: PageProps<UserID | RegistrationError | null>,
|
||||||
|
) {
|
||||||
|
if (typeof userId == "string") {
|
||||||
|
return RegistrationSuccessful(userId);
|
||||||
|
} else {
|
||||||
|
return RegistrationForm(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function RegistrationSuccessful(_userId: UserID) {
|
||||||
return (
|
return (
|
||||||
<form class="flex flex-col max-w-lg" method="post">
|
<Page>
|
||||||
<label for="username">Username</label>
|
<p>
|
||||||
<input type="text" name="username" />
|
You're all signed up! Let's go <a href="/login">log in</a>!
|
||||||
<label for="password">Password</label>
|
</p>
|
||||||
<input type="password" name="password" />
|
</Page>
|
||||||
<input class="mt-2" type="submit" value="Register" />
|
);
|
||||||
</form>
|
}
|
||||||
|
|
||||||
|
function RegistrationForm(props?: RegistrationError | null) {
|
||||||
|
console.log(props);
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
{props != null &&
|
||||||
|
<p class="text-red-500">{props.message}</p>}
|
||||||
|
<form class="flex flex-col max-w-lg" method="post">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input class="bg-gray-800 px-4 py-2" type="text" name="username" />
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input class="bg-gray-800 px-4 py-2" type="password" name="password" />
|
||||||
|
<input
|
||||||
|
class="bg-gray-800 px-4 p-2 mt-2"
|
||||||
|
type="submit"
|
||||||
|
value="Register"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,6 @@ import { apply } from "twind";
|
||||||
export default {
|
export default {
|
||||||
selfURL: import.meta.url,
|
selfURL: import.meta.url,
|
||||||
preflight: {
|
preflight: {
|
||||||
body: apply("bg-white text-black dark:bg-gray-900 dark:text-white"),
|
body: apply("bg-white text-black dark:(bg-gray-900 text-white)"),
|
||||||
},
|
},
|
||||||
} as Options;
|
} as Options;
|
||||||
|
|
Loading…
Reference in a new issue