Basics coming together...

This commit is contained in:
Daniel Flanagan 2022-10-08 02:53:13 -05:00
parent 1099ea1785
commit c97c3f7b69
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
7 changed files with 66 additions and 46 deletions

21
components/Note.tsx Normal file
View file

@ -0,0 +1,21 @@
import { type Note } from "@/types.ts";
export function NoteItem(
{ id, createdAt, content, userId, userDisplayName, userUsername }: Note,
) {
return (
<div class="my-4" key={id}>
<span>
<a href={`/note/${id}`} class="text-blue-500">Note {id}</a>
{" created by "}
{!userId
? "Anonymous"
: <a href={`/user/${userId}`}>{userDisplayName || userUsername}</a>}
{` at ${createdAt.toLocaleString()}`}
</span>
<blockquote class="mt-2 pl-4 border-l(solid 4)">
<pre>{content}</pre>
</blockquote>
</div>
);
}

View file

@ -53,13 +53,15 @@ export async function queryObject<T>(
sql: string, sql: string,
args?: QueryArguments, args?: QueryArguments,
): Promise<QueryObjectResult<T> | null> { ): Promise<QueryObjectResult<T> | null> {
return await dbOp(async (connection) => return await dbOp(async (connection) => {
await connection.queryObject<T>({ const result = await connection.queryObject<T>({
camelcase: true, camelcase: true,
text: sql.trim(), text: sql.trim(),
args, args,
}) });
); console.debug(result);
return result;
});
} }
export async function queryArray<T extends []>( export async function queryArray<T extends []>(
@ -76,8 +78,8 @@ export async function queryArray<T extends []>(
export async function listNotes(): Promise<Note[] | null> { export async function listNotes(): Promise<Note[] | null> {
return someRows( return someRows(
await queryObject<Note>( await queryObject<Note & User>(
"select * from note order by created_at desc", 'select u.username as user_username, u.display_name as user_display_name, n.* from note n left join "user" u on u.id = n.user_id order by n.created_at desc',
), ),
); );
} }
@ -98,12 +100,12 @@ export async function getNote(
type Ungenerated<T> = Omit<T, keyof Identifiable | keyof Timestamped>; type Ungenerated<T> = Omit<T, keyof Identifiable | keyof Timestamped>;
export async function createNote( export async function createNote(
{ content }: Ungenerated<Note>, { content, userId }: Ungenerated<Note>,
): Promise<Note | null> { ): Promise<Note | null> {
return singleRow( return singleRow(
await queryObject<Note>( await queryObject<Note>(
"insert into note (content) values ($1) returning *", "insert into note (content, user_id) values ($1, $2) returning *",
[content], [content, userId],
), ),
); );
} }

View file

@ -1,28 +1,24 @@
import { MiddlewareHandlerContext } from "$fresh/server.ts"; import { MiddlewareHandlerContext } from "$fresh/server.ts";
import { deleteCookie, getCookies } from "$std/http/cookie.ts"; import { deleteCookie, getCookies } from "$std/http/cookie.ts";
import { getUserFromNonExpiredLoginToken } from "@/db/mod.ts"; import { getUserFromNonExpiredLoginToken } from "@/db/mod.ts";
import { type ContextState } from "@/types.ts";
interface State {
data: string;
}
export async function handler( export async function handler(
request: Request, request: Request,
ctx: MiddlewareHandlerContext<State>, ctx: MiddlewareHandlerContext<ContextState>,
) { ) {
ctx.state.data = "";
let hasBadAuthCookie = false; let hasBadAuthCookie = false;
const { lsauth } = getCookies(request.headers); const { lsauth } = getCookies(request.headers);
console.log("lsauth cookie:", lsauth); console.log("lsauth cookie:", lsauth);
if (lsauth) { if (lsauth) {
const user = await getUserFromNonExpiredLoginToken(lsauth); const user = await getUserFromNonExpiredLoginToken(lsauth);
if (!user) hasBadAuthCookie = true; if (!user) hasBadAuthCookie = true;
else {ctx.state.data += "user:" + JSON.stringify({ else {
id: user.id, ctx.state.user = user;
username: user.username, delete ctx.state.user.createdAt;
displayName: user.displayName || user.username, delete ctx.state.user.updatedAt;
}) + delete ctx.state.user.passwordDigest;
"\n";} }
} }
const resp = await ctx.next(); const resp = await ctx.next();

View file

@ -1,17 +1,19 @@
import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { Page } from "@/components/Page.tsx"; import { Page } from "@/components/Page.tsx";
// import { getToken, getUser } from "@/db/mod.ts"; // import { getToken, getUser } from "@/db/mod.ts";
// import * as base64 from "$std/encoding/base64.ts"; // import * as base64 from "$std/encoding/base64.ts";
import { getCookies } from "$std/http/cookie.ts"; import { type ContextState } from "@/types.ts";
import { type User } from "@/types.ts";
export const handler: Handlers<unknown> = { export const handler: Handlers<unknown, ContextState> = {
async GET(request: Request, context) { async GET(_request: Request, context) {
return await context.render(context.state.data); const user: Partial<ContextState["user"]> = context.state.user;
delete user.passwordDigest;
return await context.render(context.state.user);
}, },
}; };
export default function Dashboard({ data }: PageProps) { export default function Dashboard({ data }: PageProps) {
console.log(data);
if (data) { if (data) {
return You(data); return You(data);
} else { } else {
@ -23,7 +25,7 @@ function You(data: unknown) {
return ( return (
<Page> <Page>
<p> <p>
You are <pre>{data}</pre>. You are <pre>{JSON.stringify(data)}</pre>.
</p> </p>
</Page> </Page>
); );

View file

@ -2,6 +2,7 @@ import { Handlers, PageProps } from "$fresh/server.ts";
import { listNotes } from "@/db/mod.ts"; import { listNotes } from "@/db/mod.ts";
import { Page } from "@/components/Page.tsx"; import { Page } from "@/components/Page.tsx";
import { type Note } from "@/types.ts"; import { type Note } from "@/types.ts";
import { NoteItem } from "@/components/Note.tsx";
export const handler: Handlers<Note[]> = { export const handler: Handlers<Note[]> = {
async GET(_request, context) { async GET(_request, context) {
@ -19,17 +20,7 @@ export default function NotesPage({ data: notes }: PageProps<Note[]>) {
</textarea> </textarea>
<input class="mt-2" type="submit" value="Post" /> <input class="mt-2" type="submit" value="Post" />
</form> </form>
{notes.map(({ id, content, createdAt }) => ( {notes.map(NoteItem)}
<div class="my-4" key={id}>
<span>
<a href={`/note/${id}`} class="text-blue-500">Note {id}</a>{" "}
created at {createdAt.toLocaleString()}
</span>
<blockquote class="mt-2 pl-4 border-l(solid 4)">
<pre>{content}</pre>
</blockquote>
</div>
))}
</Page> </Page>
); );
} }

View file

@ -1,13 +1,14 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { createNote } from "@/db/mod.ts"; import { createNote } from "@/db/mod.ts";
import { Page } from "@/components/Page.tsx"; import { Page } from "@/components/Page.tsx";
import { type Note } from "@/types.ts"; import { type ContextState, type Note } from "@/types.ts";
export const handler: Handlers<Note> = { export const handler: Handlers<Note, ContextState> = {
async POST(request, context) { async POST(request, context) {
const userId = context.state.user ? context.state.user.id : null;
const content = (await request.formData()).get("content"); const content = (await request.formData()).get("content");
if (!content) throw "no content provided"; if (!content) throw "no content provided";
const note = await createNote({ content: content.toString() }); const note = await createNote({ userId, content: content.toString() });
if (!note) throw "no note created"; if (!note) throw "no note created";
return await context.render(note); return await context.render(note);
}, },

View file

@ -12,10 +12,6 @@ export interface Updated {
export type Timestamped = Created & Updated; export type Timestamped = Created & Updated;
export interface Note extends Identifiable, Timestamped {
content: string;
}
export interface Team extends Identifiable, Timestamped { export interface Team extends Identifiable, Timestamped {
displayName: string; displayName: string;
} }
@ -26,6 +22,13 @@ export interface User extends Identifiable, Timestamped {
displayName?: string; displayName?: string;
} }
export interface Note extends Identifiable, Timestamped {
content: string;
userId: User["id"] | null;
userUsername?: User["username"];
userDisplayName?: User["displayName"];
}
type IdentifierFor<T extends Identifiable> = T["id"]; type IdentifierFor<T extends Identifiable> = T["id"];
export interface Token extends Created { export interface Token extends Created {
@ -37,3 +40,7 @@ export interface Token extends Created {
/** 32 bytes base64-encoded */ /** 32 bytes base64-encoded */
export type TokenDigest = string; export type TokenDigest = string;
export interface ContextState {
user?: Omit<User, "passwordDigest" | keyof Timestamped> & Partial<User>;
}