basic auth

main
Ethan Niser 2023-09-14 21:51:55 -05:00
parent 0b1c246fe2
commit 32f03a452b
14 changed files with 184 additions and 25 deletions

@ -25,7 +25,7 @@ export const auth = lucia({
getUserAttributes: (data) => {
return {
username: data.username,
email: data.email,
};
},
});

@ -7,6 +7,7 @@ const env = createEnv({
DATABASE_URL: z.string().min(1),
DATABASE_AUTH_TOKEN: z.string().min(1),
NODE_ENV: z.enum(["development", "production"]),
COOKIE_SECRET: z.string().min(1),
},
runtimeEnv: process.env,
});

@ -5,6 +5,7 @@ import { config } from "../config";
import { db } from "../db";
import "@kitajs/html/register";
import "@kitajs/html/htmx";
import { auth } from "../auth";
// const stream = pretty({
// colorize: true,
@ -12,6 +13,10 @@ import "@kitajs/html/htmx";
export const ctx = new Elysia({
name: "@app/ctx",
cookie: {
secrets: config.env.COOKIE_SECRET,
sign: "session",
},
})
// .use(
// logger({
@ -21,6 +26,7 @@ export const ctx = new Elysia({
// )
.decorate("db", db)
.decorate("config", config)
.decorate("auth", auth)
.decorate(
"html",
(html: string) =>

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

@ -1,5 +1,75 @@
import { Elysia } from "elysia";
import { Elysia, t } from "elysia";
import { ctx } from "../context";
import { set } from "zod";
class DuplicateEmailError extends Error {
constructor() {
super("Duplicate email");
}
}
export const authController = new Elysia({
prefix: "/auth",
}).post("/signup", "signup");
})
.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: {},
});
const sessionCookie = auth.createSessionCookie(session);
set.headers["Set-Cookie"] = sessionCookie.serialize();
set.redirect = "/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";
}
},
}
);

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

@ -1,14 +1,12 @@
import { Elysia } from "elysia";
import { swagger } from "@elysiajs/swagger";
// import { swagger } from "@elysiajs/swagger";
import { staticPlugin } from "@elysiajs/static";
import { api } from "./controllers/*";
import { config } from "./config";
import { pages } from "./pages/*";
const app = new Elysia()
// @ts-expect-error idk why this is broken
.use(swagger())
// @ts-expect-error idk why this is broken
// .use(swagger())
.use(staticPlugin())
.use(api)
.use(pages)
@ -18,6 +16,9 @@ const app = new Elysia()
console.log("🦊 Triggering Live Reload");
}
})
.onError(({ code, error, request }) => {
console.error(` ${request.method} ${request.url}`, code, error);
})
.listen(3000);
export type App = typeof app;

@ -0,0 +1,5 @@
import Elysia from "elysia";
import { signup } from "./signup";
import { profile } from "./profile";
export const authGroup = new Elysia().use(signup).use(profile);

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

@ -0,0 +1,54 @@
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,4 +1,5 @@
import Elysia from "elysia";
import { index } from "./index";
import { authGroup } from "./(auth)/*";
export const pages = new Elysia().use(index);
export const pages = new Elysia().use(index).use(authGroup);

@ -1,13 +1,15 @@
type RoutesByType<
Schema extends Record<string, unknown>,
Schema extends Record<string, any>, // Ensure keys are strings
Type extends "get" | "post" | "put" | "delete" | "patch"
> = RouterPattern<
RemoveSlash<
keyof {
[key in keyof Schema as Schema[key] extends { [key in Type]: unknown }
? key
: never]: true;
}
string &
keyof {
// Constrain to strings here
[key in keyof Schema as Schema[key] extends { [key in Type]: unknown }
? key
: never]: true;
}
>
>;
@ -26,15 +28,15 @@ type RouterPattern<T extends string> =
? `${Start}${string}`
: T;
declare namespace JSX {
type Schema = import("../main").App["meta"]["schema"];
type Schema = import("../main").App["schema"];
type PostRoutes = RoutesByType<Schema, "post">;
type GetRoutes = RoutesByType<Schema, "get">;
type PutRoutes = RoutesByType<Schema, "put">;
type DeleteRoutes = RoutesByType<Schema, "delete">;
type PatchRoutes = RoutesByType<Schema, "patch">;
type PostRoutes = RoutesByType<Schema, "post">;
type GetRoutes = RoutesByType<Schema, "get">;
type PutRoutes = RoutesByType<Schema, "put">;
type DeleteRoutes = RoutesByType<Schema, "delete">;
type PatchRoutes = RoutesByType<Schema, "patch">;
declare namespace JSX {
interface HtmlTag extends Htmx.Attributes {
["hx-get"]?: GetRoutes;
["hx-post"]?: PostRoutes;

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

File diff suppressed because one or more lines are too long