little twitter clone!

main
Ethan Niser 2023-09-21 00:35:52 -05:00
parent aab4bbb4dc
commit fbc91a664c
30 changed files with 469 additions and 557 deletions

Binary file not shown.

@ -40,11 +40,11 @@
"@libsql/client": "0.3.5-pre.9",
"@lucia-auth/adapter-sqlite": "^2.0.0",
"@t3-oss/env-core": "^0.6.1",
"@tlscipher/holt": "^1.0.4",
"beth-stack": "0.0.20",
"@tlscipher/holt": "1.1.0",
"beth-stack": "0.0.22",
"drizzle-orm": "^0.28.6",
"drizzle-typebox": "^0.1.1",
"elysia": "0.7.1",
"elysia": "0.7.4",
"libsql": "^0.1.13",
"lucia": "^2.6.0",
"pino-pretty": "^10.2.0",

@ -1,6 +1,5 @@
import { libsql } from "@lucia-auth/adapter-sqlite";
import { lucia } from "lucia";
import { elysia, web } from "lucia/middleware";
import { lucia, Middleware } from "lucia";
import { config } from "../config";
import { client } from "../db";
@ -11,21 +10,45 @@ const envAliasMap = {
const envAlias = envAliasMap[config.env.NODE_ENV];
type ElysiaContext = {
request: Request;
set: {
headers: Record<string, string> & {
["Set-Cookie"]?: string | string[];
};
status?: number | undefined | string;
redirect?: string | undefined;
};
};
export const elysia = (): Middleware<[ElysiaContext]> => {
return ({ args }) => {
const [{ request, set }] = args;
return {
request,
setCookie: (cookie) => {
const setCookieHeader = set.headers["Set-Cookie"] ?? [];
const setCookieHeaders: string[] = Array.isArray(setCookieHeader)
? setCookieHeader
: [setCookieHeader];
setCookieHeaders.push(cookie.serialize());
set.headers["Set-Cookie"] = setCookieHeaders;
},
};
};
};
export const auth = lucia({
env: envAlias,
middleware: web(),
sessionCookie: {
expires: false,
},
middleware: elysia(),
adapter: libsql(client, {
user: "user",
key: "user_key",
session: "user_session",
}),
getUserAttributes: (data) => {
return {
email: data.email,
handle: data.handle,
};
},
});

@ -0,0 +1,13 @@
import Elysia from "elysia";
import { ctx } from "../context";
export const authed = new Elysia({
name: "@app/plugins/authed",
})
.use(ctx)
.derive(async (ctx) => {
const authRequest = ctx.auth.handleRequest(ctx);
const session = await authRequest.validate();
return { session };
});

@ -1,4 +1,3 @@
import { htmxExtensionScript } from "beth-stack";
import { liveReloadScript } from "beth-stack/dev";
import { type PropsWithChildren } from "beth-stack/jsx";
import { config } from "../config";
@ -13,7 +12,7 @@ export const BaseHtml = ({ children }: PropsWithChildren) => (
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>THE BETH STACK</title>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<script>{htmxExtensionScript}</script>
<script src="https://unpkg.com/htmx.org/dist/ext/response-targets.js"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.11"></script>
<link
rel="stylesheet"
@ -22,6 +21,8 @@ export const BaseHtml = ({ children }: PropsWithChildren) => (
<link rel="stylesheet" href="/public/dist/unocss.css" />
<script>{safeScript}</script>
</head>
<body hx-ext="revalidate">{children}</body>
<body hx-boost="true" hx-ext="response-targets">
{children}
</body>
</html>
);

@ -1,14 +0,0 @@
function Component({ name }: { name: string }) {
return (
<p>
<h1 safe>
<Foo />
{name}
</h1>
</p>
);
}
function Foo() {
return <p>hi</p>;
}

@ -1,58 +0,0 @@
import type { Todo } from "../db/schema/todos";
export function TodoItem({ content, completed, id }: Todo) {
return (
<div class="flex flex-row space-x-3">
<p safe>{content}</p>
<input
type="checkbox"
checked={completed}
hx-post={`/api/todos/toggle/${id}`}
hx-swap="outerHTML"
hx-target="closest div"
class="p4 pt-2"
/>
<button
class="text-red-500"
hx-delete={`/api/todos/${id}`}
hx-swap="outerHTML"
hx-target="closest div"
>
X
</button>
</div>
);
}
export function TodoList({ todos }: { todos: Todo[] }) {
return (
<div safe>
{todos.map((todo) => (
<TodoItem {...todo} />
))}
<TodoForm />
</div>
);
}
export function TodoForm() {
return (
<form
class="flex flex-row space-x-3"
hx-post="/api/todos"
hx-swap="beforebegin"
_="on submit target.reset()"
>
<select name="content" class="border border-black">
<option value="" disabled={true} selected="true">
Select a Todo
</option>
<option value="beth">Learn the BETH stack</option>
<option value="vim">Learn vim</option>
<option value="like">Like the video</option>
<option value="sub">Subscribe to Ethan</option>
</select>
<button type="submit">Add</button>
</form>
);
}

@ -0,0 +1,56 @@
import { db } from "../db";
import { tweets, type Tweet } from "../db/schema/tweets";
export function TweetCard({ authorId, createdAt, content }: Tweet) {
return (
<div class="rounded-lg border p-4 shadow-md">
<h2 class="text-xl font-bold" safe>
{authorId}
</h2>
<p class="text-gray-700" safe>
{content}
</p>
<span class="text-sm text-gray-500">
{new Date(createdAt).toLocaleString()}
</span>
</div>
);
}
export async function TweetList() {
const tweetData = await db.select().from(tweets).limit(10);
return (
<div class="space-y-4" id="tweetList">
{tweetData.map((tweet) => (
<TweetCard {...tweet} />
))}
</div>
);
}
export function TweetCreationForm() {
return (
<div class="rounded-lg border p-4 shadow-md">
<h2 class="mb-4 text-xl font-bold">Create a new Tweet</h2>
<form hx-post="/api/tweets" hx-swap="afterend" hx-target="#tweetList">
<label class="mb-2 block text-sm font-bold" for="content">
Tweet:
</label>
<textarea
class="w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow"
name="content"
rows="3"
required="true"
_="on submit reset me"
></textarea>
<button
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
type="submit"
>
Post Tweet
</button>
</form>
</div>
);
}

@ -1,4 +1,5 @@
import { logger } from "@bogeychan/elysia-logger";
import { Logger } from "@bogeychan/elysia-logger/types";
import { cron } from "@elysiajs/cron";
import { HoltLogger } from "@tlscipher/holt";
import { bethStack } from "beth-stack/elysia";
@ -12,47 +13,74 @@ const stream = pretty({
colorize: true,
});
const loggerConfig =
config.env.NODE_ENV === "development"
? {
level: config.env.LOG_LEVEL,
stream,
}
: { level: config.env.LOG_LEVEL };
function optionallyUse(condition: boolean, middleware: any): any {
return condition ? middleware : (a: any) => a;
}
export const ctx = new Elysia({
name: "@app/ctx",
})
// .use(
// bethStack({
// // log: false,
// returnStaleWhileRevalidate: false,
// }),
// )
.decorate("db", db)
.decorate("config", config)
.decorate("auth", auth)
.use(bethStack())
.use(logger(loggerConfig))
.use(
logger({
level: config.env.LOG_LEVEL,
stream,
}),
// @ts-expect-error
config.env.NODE_ENV === "development"
? new HoltLogger().getLogger()
: (a) => a,
)
.use(new HoltLogger().getLogger())
.use(
// @ts-expect-error
config.env.DATABASE_CONNECTION_TYPE === 'local-replica' ? cron({
name: "heartbeat",
pattern: "*/2 * * * * *",
run() {
const now = performance.now();
console.log("Syncing database...");
void client.sync().then(() => {
console.log(`Database synced in ${performance.now() - now}ms`);
});
},
}) : (a) => a,
config.env.DATABASE_CONNECTION_TYPE === "local-replica"
? cron({
name: "heartbeat",
pattern: "*/2 * * * * *",
run() {
const now = performance.now();
console.log("Syncing database...");
void client.sync().then(() => {
console.log(`Database synced in ${performance.now() - now}ms`);
});
},
})
: (a) => a,
)
.decorate("db", db)
.decorate("config", config)
.decorate("auth", auth);
// .onStart(({ log }) => log.info("Server starting"));
// .onStop(({ log }) => log.info("Server stopping"))
// .onRequest(({ log, request }) => {
// log.debug(`Request received: ${request.method}: ${request.url}`);
// });
// .onResponse(({ log, request, set }) => {
// log.debug(
// `Response sent: ${request.method}: ${request.url} with status ${set.status}`
// );
// });
// .onError(({ log, error }) => log.error(error));
.use(
// @ts-expect-error
config.env.NODE_ENV === "production"
? (
e: Elysia<
"",
{
request: {
log: Logger;
};
store: {};
}
>,
) =>
e
.onStart(({ log }) => log.info("Server starting"))
.onStop(({ log }) => log.info("Server stopping"))
.onRequest(({ log, request }) => {
console.log(typeof log);
log.debug(`Request received: ${request.method}: ${request.url}`);
})
.onResponse(({ log, request, set }) => {
log.debug(
`Response sent: ${request.method}: ${request.url} with status ${set.status}`,
);
})
.onError(({ log, error }) => log.error(error))
: (a) => a,
);

@ -1,10 +1,9 @@
import Elysia from "elysia";
import { todosController } from "./todos";
// import { authController } from "./auth";
import { authController } from "./auth";
import { tweetsController } from "./tweets";
export const api = new Elysia({
prefix: "/api",
name: "@controllers",
}).use(todosController);
// .use(authController);
})
.use(authController)
.use(tweetsController);

@ -1,128 +1,108 @@
// import { Elysia, t } from "elysia";
// import { ctx } from "../context";
// import { set } from "zod";
// import { LuciaError } from "lucia";
import { Elysia, t } from "elysia";
import { LuciaError } from "lucia";
import { ctx } from "../context";
// class DuplicateEmailError extends Error {
// constructor() {
// super("Duplicate email");
// }
// }
class DuplicateEmailError extends Error {
constructor() {
super("Duplicate email");
}
}
// export const authController = new Elysia({
// prefix: "/auth",
// })
// .use(ctx)
// .post(
// "/signup",
// async ({ body: { email, password }, auth, set }) => {
// const user = await auth
// .createUser({
// key: {
// providerId: "email", // auth method
// providerUserId: email.toLowerCase(), // unique id when using "email" auth method
// password, // hashed by Lucia
// },
// attributes: {
// email,
// },
// })
// .catch((err) => {
// if (err.code === "SQLITE_CONSTRAINT") {
// throw new DuplicateEmailError();
// } else {
// throw err;
// }
// });
// const session = await auth.createSession({
// userId: user.userId,
// attributes: {},
// });
export const authController = new Elysia({
prefix: "/auth",
})
.use(ctx)
.post(
"/signInOrUp",
async ({ body: { handle, password, action }, auth, set }) => {
let user;
// const sessionCookie = auth.createSessionCookie(session);
// Decide action based on the "action" param sent in body
if (action === "signup") {
user = await auth
.createUser({
key: {
providerId: "basic",
providerUserId: handle.toLowerCase(),
password,
},
attributes: {
handle,
},
})
.catch((err) => {
if (err.code === "SQLITE_CONSTRAINT") {
throw new DuplicateEmailError();
} else {
throw err;
}
});
} else if (action === "signin") {
user = await auth.useKey("basic", handle.toLowerCase(), password);
} else {
throw new Error("Invalid action");
}
// // cookie.session?.set(sessionCookie);
const session = await auth.createSession({
userId: user.userId,
attributes: {},
});
const sessionCookie = auth.createSessionCookie(session);
// set.headers["Set-Cookie"] = sessionCookie.serialize();
// set.headers["HX-Location"] = "/profile";
// },
// {
// body: t.Object({
// email: t.String({
// minLength: 5,
// maxLength: 30,
// }),
// password: t.String({
// minLength: 6,
// maxLength: 255,
// }),
// }),
// error({ code, error, set }) {
// if (code === "VALIDATION") {
// console.log("sign up validation error");
// console.log(error);
// set.status = 400;
// return "Invalid email or password";
// } else if (error instanceof DuplicateEmailError) {
// console.log("sign up duplicate email error");
// console.log(error);
// set.status = 400;
// return "Email already exists";
// } else {
// console.log("sign up error");
// console.log(error);
// set.status = 500;
// return "Internal server error";
// }
// },
// }
// )
// .post(
// "/signin",
// async ({ body: { email, password }, auth, set }) => {
// const user = await auth.useKey("email", email.toLowerCase(), password);
set.headers["Set-Cookie"] = sessionCookie.serialize();
set.headers["HX-Location"] = "/";
},
{
body: t.Object({
handle: t.String({
minLength: 1,
maxLength: 10,
}),
password: t.String({
minLength: 4,
maxLength: 255,
}),
action: t.Enum({
signup: "signup",
signin: "signin",
}), // Enum to validate action type
}),
error({ code, error, set }) {
let errorMessage = "";
// const session = await auth.createSession({
// userId: user.userId,
// attributes: {},
// });
// const sessionCookie = auth.createSessionCookie(session);
if (code === "VALIDATION") {
errorMessage = "Invalid email or password";
} else if (error instanceof DuplicateEmailError) {
errorMessage = "Email already exists";
} else if (
error instanceof LuciaError &&
(error.message === "AUTH_INVALID_KEY_ID" ||
error.message === "AUTH_INVALID_PASSWORD")
) {
errorMessage = "Invalid email or password";
} else {
errorMessage = "Internal server error";
}
// set.headers["Set-Cookie"] = sessionCookie.serialize();
// set.headers["HX-Location"] = "/profile";
// },
// {
// body: t.Object({
// email: t.String({
// minLength: 5,
// maxLength: 30,
// }),
// password: t.String({
// minLength: 6,
// maxLength: 255,
// }),
// }),
// error({ code, error, set }) {
// if (code === "VALIDATION") {
// console.log("sign up validation error");
// console.log(error);
// set.status = 400;
// return "Invalid email or password";
// } else if (
// error instanceof LuciaError &&
// (error.message === "AUTH_INVALID_KEY_ID" ||
// error.message === "AUTH_INVALID_PASSWORD")
// ) {
// console.log("sign in invalid email or password error");
// console.log(error);
// set.status = 400;
// return "Invalid email or password";
// } else {
// console.log("sign up error");
// console.log(error);
// set.status = 500;
// return "Internal server error";
// }
// },
// }
// );
set.status = "Unauthorized"; // set the status to 400 for all errors for simplicity
return `${errorMessage}`;
},
},
)
.post("/signout", async (ctx) => {
const authRequest = ctx.auth.handleRequest(ctx);
const session = await authRequest.validate();
if (!session) {
ctx.set.status = "Unauthorized";
return "You are not logged in";
}
await ctx.auth.invalidateSession(session.sessionId);
const sessionCookie = ctx.auth.createSessionCookie(null);
ctx.set.headers["Set-Cookie"] = sessionCookie.serialize();
ctx.set.headers["HX-Location"] = "/";
});

@ -1,98 +0,0 @@
import { eq } from "drizzle-orm";
import { Elysia, t } from "elysia";
import { TodoForm, TodoItem, TodoList } from "../components/todos";
import { ctx } from "../context";
import { client, db } from "../db";
import { insertTodoSchema, todos } from "../db/schema/todos";
export const todosController = new Elysia({
prefix: "/todos",
name: "@controllers/todos",
})
.use(ctx)
.get("/", async () => {
const data = await db.select().from(todos).limit(10);
return <TodoList todos={data} />;
})
.post(
"/toggle/:id",
async ({ params }) => {
const [oldTodo] = await db
.select()
.from(todos)
.where(eq(todos.id, params.id));
if (!oldTodo) {
throw new Error("Todo not found");
}
const [newTodo] = await db
.update(todos)
.set({ completed: !oldTodo.completed })
.where(eq(todos.id, params.id))
.returning();
if (!newTodo) {
throw new Error("Todo not found");
}
client.sync();
console.log("returning");
return <TodoItem {...newTodo} />;
},
{
params: t.Object({
id: t.Numeric(),
}),
},
)
.delete(
"/:id",
async ({ params }) => {
await db.delete(todos).where(eq(todos.id, params.id));
},
{
params: t.Object({
id: t.Numeric(),
}),
},
)
.post(
"",
async ({ body }) => {
const content = {
beth: "Learn the BETH stack",
vim: "Learn vim",
like: "Like the video",
sub: "Subscribe to Ethan",
};
const [newTodo] = await db
.insert(todos)
.values({ content: content[body.content] })
.returning();
if (!newTodo) {
throw new Error("Todo not found");
}
// const now2 = performance.now();
// client.sync().then(() => {
// console.log("synced in", performance.now() - now2);
// console.log("total time", performance.now() - now);
// });
return <TodoItem {...newTodo} />;
},
{
body: t.Object({
content: t.Union([
t.Literal("beth"),
t.Literal("vim"),
t.Literal("like"),
t.Literal("sub"),
]),
}),
},
);

@ -0,0 +1,48 @@
import { Elysia, t } from "elysia";
import { authed } from "../auth/middleware";
import { TweetCard } from "../components/tweets";
import { ctx } from "../context";
import { tweets } from "../db/schema/tweets";
export const tweetsController = new Elysia({
prefix: "/tweets",
})
.use(ctx)
.derive(async (ctx) => {
const authRequest = ctx.auth.handleRequest(ctx);
const session = await authRequest.validate();
return { session };
})
.post(
"/",
async ({ session, db, body, set, log }) => {
if (!session) {
set.status = "Unauthorized";
set.headers["HX-Redirect"] = "/signin";
return "Sign in to post a tweet.";
}
const [tweet] = await db
.insert(tweets)
.values({
authorId: session.user.userId,
content: body.content,
})
.returning();
if (!tweet) {
throw new Error("Failed to create tweet");
}
return <TweetCard {...tweet} />;
},
{
body: t.Object({
content: t.String({
minLength: 1,
maxLength: 280,
}),
}),
},
);

@ -2,7 +2,7 @@ import { blob, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
email: text("email").notNull(),
handle: text("handle").notNull(),
// other user attributes
});

@ -1,2 +1,2 @@
export { todos } from "./todos";
export { tweets } from "./tweets";
export * from "./auth";

@ -1,14 +0,0 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-typebox";
export const todos = sqliteTable("todos", {
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
content: text("content").notNull(),
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
});
export type Todo = typeof todos.$inferSelect;
export type InsertTodo = typeof todos.$inferInsert;
export const insertTodoSchema = createInsertSchema(todos);
export const selectTodoSchema = createSelectSchema(todos);

@ -0,0 +1,24 @@
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-typebox";
export const tweets = sqliteTable(
"tweet",
{
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
authorId: text("author_id").notNull(),
content: text("content").notNull(),
createdAt: integer("createdAt", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => {
return {
authorIdx: index("author_idx").on(table.authorId),
};
},
);
export type Tweet = typeof tweets.$inferSelect;
export type InsertTweet = typeof tweets.$inferInsert;
export const insertTweetSchema = createInsertSchema(tweets);
export const selectTweetSchema = createSelectSchema(tweets);

@ -1,14 +0,0 @@
import { db } from ".";
import { todos } from "./schema/todos";
await db.batch([
db.insert(todos).values({
content: "Learn the beth stack",
}),
db.insert(todos).values({
content: "Learn vim",
}),
db.insert(todos).values({
content: "subscribe to ethan",
}),
]);

@ -2,25 +2,26 @@ import { staticPlugin } from "@elysiajs/static";
// import { swagger } from "@elysiajs/swagger";
import { Elysia } from "elysia";
import { config } from "./config";
import { ctx } from "./context";
import { api } from "./controllers/*";
import { pages } from "./pages/*";
const app = new Elysia({
name: "@app/app",
})
const app = new Elysia()
// .use(swagger())
// @ts-expect-error
.use(staticPlugin())
.use(api)
.use(pages)
.onStart(({log}) => {
.onStart(({ log }) => {
if (config.env.NODE_ENV === "development") {
void fetch("http://localhost:3001/restart");
log.debug("🦊 Triggering Live Reload");
// log.debug("🦊 Triggering Live Reload");
console.log("🦊 Triggering Live Reload");
}
})
.onError(({ code, error, request, log }) => {
log.error(` ${request.method} ${request.url}`, code, error);
// log.error(` ${request.method} ${request.url}`, code, error);
console.error(error);
})
.listen(3000);

@ -1,11 +1,6 @@
import Elysia from "elysia";
// import { signup } from "./signup";
import { profile } from "./profile";
// import { signin } from "./signin";
import { signin } from "./signin";
export const authGroup = new Elysia({
name: "@pages/auth/root",
})
// .use(signup).use(signin)
.use(profile);
export const authGroup = new Elysia().use(signin);

@ -1,20 +0,0 @@
import Elysia from "elysia";
import { BaseHtml } from "../../components/base";
import { ctx } from "../../context";
export const profile = new Elysia({
name: "@pages/auth/profile",
})
.use(ctx)
.get("/profile", async ({ auth, html, request }) => {
const authRequest = auth.handleRequest(request);
const session = await authRequest.validate();
return html(() =>
session ? (
<div>Hello {session.user.email}</div>
) : (
<div>Not logged in</div>
),
);
});

@ -1,54 +1,74 @@
// import Elysia from "elysia";
// import { BaseHtml } from "../../components/base";
// import { ctx } from "../../context";
import Elysia from "elysia";
import { BaseHtml } from "../../components/base";
import { ctx } from "../../context";
// export const signin = new Elysia().use(ctx).get("/signin", ({ html }) =>
// html(
// <BaseHtml>
// <div class="flex w-full h-screen bg-gray-200 justify-center items-center">
// <form
// hx-post="/api/auth/signin"
// hx-swap="afterend"
// class="bg-white p-8 rounded-lg shadow-md w-96"
// >
// <div class="mb-4">
// <label
// for="email"
// class="block text-sm font-medium text-gray-600 mb-2"
// >
// Email
// </label>
// <input
// type="text"
// name="email"
// id="email"
// placeholder="Enter your email"
// class="w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent"
// />
// </div>
// <div class="mb-4">
// <label
// for="password"
// class="block text-sm font-medium text-gray-600 mb-2"
// >
// Password
// </label>
// <input
// type="password"
// name="password"
// id="password"
// placeholder="Enter your password"
// class="w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent"
// />
// </div>
// <button
// type="submit"
// class="w-full bg-indigo-600 text-white p-2 rounded-md hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-50"
// >
// Sign In
// </button>
// </form>
// </div>
// </BaseHtml>
// )
// );
export const signin = new Elysia().use(ctx).get("/signin", ({ html }) =>
html(() => (
<BaseHtml>
<div class="flex h-screen w-full flex-col items-center justify-center bg-gray-200">
<div class="p-4">
<a
href="/"
class="text-indigo-600 hover:text-indigo-800 hover:underline"
>
Go Home
</a>
</div>
<form
hx-post="/api/auth/signInOrUp"
hx-swap="innerHTML"
hx-target-4xx="#errorMessage"
class="w-96 rounded-lg bg-white p-8 shadow-md"
>
<div class="mb-4">
<label
for="handle"
class="mb-2 block text-sm font-medium text-gray-600"
>
Handle
</label>
<input
type="text"
name="handle"
id="handle"
placeholder="Enter your handle"
class="w-full rounded-md border p-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
</div>
<div class="mb-4">
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-600"
>
Password
</label>
<input
type="password"
name="password"
id="password"
placeholder="Enter your password"
class="w-full rounded-md border p-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
</div>
<button
type="submit"
name="action"
value="signin"
class="mb-2 w-full rounded-md bg-indigo-600 p-2 text-white hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-50"
>
Sign In
</button>
<button
type="submit"
name="action"
value="signup"
class="w-full rounded-md bg-green-600 p-2 text-white hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-50"
>
Sign Up
</button>
<div id="errorMessage" class="mt-4 text-red-500"></div>
</form>
</div>
</BaseHtml>
)),
);

@ -1,54 +0,0 @@
// import Elysia from "elysia";
// import { BaseHtml } from "../../components/base";
// import { ctx } from "../../context";
// export const signup = new Elysia().use(ctx).get("/signup", ({ html }) =>
// html(
// <BaseHtml>
// <div class="flex w-full h-screen bg-gray-200 justify-center items-center">
// <form
// hx-post="/api/auth/signup"
// hx-swap="afterend"
// class="bg-white p-8 rounded-lg shadow-md w-96"
// >
// <div class="mb-4">
// <label
// for="email"
// class="block text-sm font-medium text-gray-600 mb-2"
// >
// Email
// </label>
// <input
// type="text"
// name="email"
// id="email"
// placeholder="Enter your email"
// class="w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent"
// />
// </div>
// <div class="mb-4">
// <label
// for="password"
// class="block text-sm font-medium text-gray-600 mb-2"
// >
// Password
// </label>
// <input
// type="password"
// name="password"
// id="password"
// placeholder="Enter your password"
// class="w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent"
// />
// </div>
// <button
// type="submit"
// class="w-full bg-indigo-600 text-white p-2 rounded-md hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-50"
// >
// Sign Up
// </button>
// </form>
// </div>
// </BaseHtml>
// )
// );

@ -1,11 +1,5 @@
import Elysia from "elysia";
import { authGroup } from "./(auth)/*";
import { index } from "./index";
import { user } from "./user/*";
export const pages = new Elysia({
name: "@pages/root",
})
.use(index)
.use(authGroup)
.use(user);
export const pages = new Elysia().use(index).use(authGroup);

@ -1,16 +1,44 @@
import { Elysia } from "elysia";
import { authed } from "../auth/middleware";
import { BaseHtml } from "../components/base";
import { TweetCreationForm, TweetList } from "../components/tweets";
import { ctx } from "../context";
export const index = new Elysia({
name: "@pages/index",
})
export const index = new Elysia()
.use(ctx)
.get("/", async ({html}) => {
.derive(async (ctx) => {
const authRequest = ctx.auth.handleRequest(ctx);
const session = await authRequest.validate();
return { session };
})
.get("/", async ({ html, session, db }) => {
return html(() => (
<BaseHtml>
<h1>hi!</h1>
<div class="flex flex-col items-center py-3">
{session ? (
<>
<h1 class="text-2xl font-bold text-gray-800" safe>
Hi! {session.user.handle}
</h1>
<button
hx-post="/api/auth/signout"
class="mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white transition duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50"
>
Sign Out
</button>
<TweetCreationForm />
</>
) : (
<a
href="/signin"
class="mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white transition duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50"
>
Sign In
</a>
)}
<TweetList />
</div>
</BaseHtml>
));
});

@ -1,7 +0,0 @@
import Elysia from "elysia";
import { id } from "./<id>";
export const user = new Elysia({
name: "@pages/user/*",
})
.use(id)

@ -1,15 +0,0 @@
import Elysia from "elysia";
import { BaseHtml } from "../../components/base";
import { ctx } from "../../context";
export const id = new Elysia({
name: "@pages/user/[id]",
})
.use(ctx)
.get("/user/:id", async ({ html, params: { id } }) => {
return html(() => (
<BaseHtml>
<h1>hi: {id}!</h1>
</BaseHtml>
));
});

@ -53,8 +53,4 @@ declare namespace JSX {
["hx-patch"]?: StartsWithApi<PatchRoutes>;
_?: string;
}
interface HtmlAnchorTag {
href?: DoesntStartWithApi<GetRoutes>;
}
}

@ -2,7 +2,7 @@
declare namespace Lucia {
type Auth = import("../auth/index").Auth;
type DatabaseUserAttributes = {
email: string;
handle: string;
};
type DatabaseSessionAttributes = {};
}

@ -21,7 +21,7 @@
"bun-types" // add Bun global
],
// non bun init
"plugins": [{ "name": "@kitajs/ts-html-plugin" }],
// "plugins": [{ "name": "@kitajs/ts-html-plugin" }],
"noUncheckedIndexedAccess": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,