Skip to content
This repository has been archived by the owner on Nov 27, 2024. It is now read-only.

Upgrade to Elysia v1 #5

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,8 @@ private/

.vercel
public/globals.css

# misc

.DS_Store
*.pem
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Config } from 'drizzle-kit';
import { env } from '~/env';
import { env } from './src/env';

export default {
schema: './src/db/schemas/*.ts',
Expand Down
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@
"db:view": "drizzle-kit studio"
},
"dependencies": {
"@auth/core": "^0.14.0",
"@auth/drizzle-adapter": "^0.3.2",
"@elysiajs/html": "^0.6.5",
"@elysiajs/static": "^0.6.0",
"@elysiajs/swagger": "^0.6.2",
"@elysiajs/html": "^1.0.2",
"@elysiajs/static": "^1.0.2",
"@elysiajs/swagger": "^1.0.3",
"@libsql/client": "^0.3.4",
"@lucia-auth/adapter-sqlite": "^3.0.1",
"arctic": "^1.2.1",
"better-sqlite3": "^8.6.0",
"drizzle-orm": "^0.28.6",
"drizzle-typebox": "^0.1.1",
"elysia": "^0.6.23",
"lucia": "^3.1.1",
"oslo": "^1.1.3",
"zod": "^3.22.2"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.1.0",
"@kitajs/ts-html-plugin": "^1.3.4",
"@tailwindcss/typography": "^0.5.10",
"@total-typescript/ts-reset": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^6.7.0",
Expand All @@ -51,7 +53,6 @@
"tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.7",
"ts-toolbelt": "^9.6.0",
"typed-html": "^3.0.1",
"typescript": "^5.2.2"
}
}
1 change: 1 addition & 0 deletions public/[email protected]

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion public/[email protected]

This file was deleted.

119 changes: 111 additions & 8 deletions src/app/api/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,116 @@
import { Auth } from '@auth/core';
import { authConfig } from '~/lib/auth';
import { generateState, OAuth2RequestError } from 'arctic';
import { generateId } from 'lucia';
import { parseCookies, serializeCookie } from 'oslo/cookie';
import { z } from 'zod';
import { users } from '~/db/schemas/auth';
import { env } from '~/env';
import { github, lucia } from '~/lib/auth';
import { db } from '~/lib/db';
import { createElysia } from '~/util/elysia';

// const githubUserSchema = z.object({
// id: z.string(),
// login: z.string(),
// });

export const routes = createElysia({ prefix: '/auth' })
.get('/*', async (ctx) => {
const res = await Auth(ctx.request, authConfig);
return res;
.get('/login/github', async (ctx) => {
const state = generateState();
const url = await github.createAuthorizationURL(state);
return new Response(null, {
status: 302,
headers: {
Location: url.toString(),
'Set-Cookie': serializeCookie('github_oauth_state', state, {
httpOnly: true,
secure: env.NODE_ENV === 'production', // set `Secure` flag in HTTPS
maxAge: 60 * 10, // 10 minutes
path: '/',
}),
},
});
})
.get('/login/github/callback', async (ctx) => {
const cookies = parseCookies(ctx.request.headers.get('Cookie') ?? '');
const stateCookie = cookies.get('github_oauth_state') ?? null;

const url = new URL(ctx.request.url);
const state = url.searchParams.get('state');
const code = url.searchParams.get('code');

// verify state
if (!state || !stateCookie || !code || stateCookie !== state) {
return new Response(null, {
status: 400,
});
}

try {
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
const githubUser = await githubUserResponse.json();

const existingUser = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.githubId, githubUser.id),
});

if (existingUser) {
const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
return new Response(null, {
status: 302,
headers: {
Location: '/',
'Set-Cookie': sessionCookie.serialize(),
},
});
}

const userId = generateId(15);
await db.insert(users).values({
id: userId,
githubId: githubUser.id,
});

const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
return new Response(null, {
status: 302,
headers: {
Location: '/',
'Set-Cookie': sessionCookie.serialize(),
},
});
} catch (e) {
console.log(e);
if (e instanceof OAuth2RequestError) {
// bad verification code, invalid credentials, etc
return new Response(null, {
status: 400,
});
}
return new Response(null, {
status: 500,
});
}
})
.post('/*', async (ctx) => {
const res = await Auth(ctx.request, authConfig);
return res;
.post('/logout', async (ctx) => {
if (!ctx.session) {
return new Response(null, {
status: 401,
});
}

await lucia.invalidateSession(ctx.session.id);
return new Response(null, {
status: 302,
headers: {
Location: '/',
'Set-Cookie': lucia.createBlankSessionCookie().serialize(),
},
});
});
9 changes: 3 additions & 6 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { html } from '@elysiajs/html';
import { swagger } from '@elysiajs/swagger';
import { SignIn, SignOut } from '~/components/auth';
import { Layout } from '~/components/Layout';
import { getSession } from '~/lib/auth';
import { createElysia } from '~/util/elysia';
import { routes as apiRoutes } from './api';
import { routes as todosRoutes } from './todos';
Expand All @@ -20,15 +19,13 @@ export const app = createElysia()
// Page routes
.use(todosRoutes)
.get('/', async (ctx) => {
const session = await getSession(ctx.request);

return (
<Layout>
<div class='px-6 py-6'>
{session ? (
<SignOut />
{ctx.session ? (
<SignOut></SignOut>
) : (
<SignIn innerText='Sign in to modify todos' />
<SignIn>Sign in to modify todos</SignIn>
)}
<div class='py-3'></div>
<div hx-get='/todos' hx-trigger='load' hx-swap='innerHTML'></div>
Expand Down
43 changes: 19 additions & 24 deletions src/app/todos/components.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,45 @@
import { Todo } from '~/db/schemas/todo';

export function TodoItem({ todo, enabled }: { todo: Todo; enabled: boolean }) {
type TodoItemProps = { todo: Todo; enabled: boolean };
export const TodoItem: Html.Component<TodoItemProps> = (props) => {
return (
<div class='flex flex-row space-x-3'>
<input
name='completed'
type='checkbox'
checked={todo.completed}
hx-patch={`/todos/${todo.id}`}
checked={props.todo.completed}
hx-patch={`/todos/${props.todo.id}`}
hx-swap='outerHTML'
hx-target='closest div'
disabled={!enabled}
disabled={!props.enabled}
/>
<p>{todo.task}</p>
<p safe>{props.todo.task}</p>
<button
class='text-red-500'
hx-delete={`/todos/${todo.id}`}
hx-delete={`/todos/${props.todo.id}`}
hx-swap='outerHTML'
hx-target='closest div'
// @ts-expect-error - incorrectly typed in @elysiajs/html
disabled={!enabled}
disabled={!props.enabled}
>
X
</button>
</div>
);
}
};

export function TodoList({
todos,
enabled,
}: {
todos: Todo[];
enabled: boolean;
}) {
type TodoListProps = { todos: Todo[]; enabled: boolean };
export const TodoList: Html.Component<TodoListProps> = (props) => {
return (
<div id='todo-list'>
{todos.toReversed().map((todo) => (
<TodoItem todo={todo} enabled={enabled} />
{props.todos.toReversed().map((todo) => (
<TodoItem todo={todo} enabled={props.enabled} />
))}
</div>
);
}
};

export function TodoForm({ enabled }: { enabled: boolean }) {
type TodoFormProps = { enabled: boolean };
export const TodoForm: Html.Component<TodoFormProps> = (props) => {
return (
<form
class='flex flex-row space-x-3'
Expand All @@ -55,7 +51,7 @@ export function TodoForm({ enabled }: { enabled: boolean }) {
name='task'
class='rounded-sm border border-black dark:text-black'
>
<option value='Go shopping' selected='true'>
<option value='Go shopping' selected>
Go shopping
</option>
<option value='Buy bread'>Buy bread</option>
Expand All @@ -64,11 +60,10 @@ export function TodoForm({ enabled }: { enabled: boolean }) {
<button
type='submit'
class='disabled:opacity-50 disabled:pointer-events-none rounded-sm bg-primary text-primary-foreground hover:bg-primary/90 px-3'
// @ts-expect-error - incorrectly typed in @elysiajs/html
disabled={!enabled}
disabled={!props.enabled}
>
Add
</button>
</form>
);
}
};
13 changes: 5 additions & 8 deletions src/app/todos/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { eq } from 'drizzle-orm';
import { insertTodoSchema, patchTodoSchema, todos } from '~/db/schemas/todo';
import { isAuthenticated } from '~/hooks/isAuthenticated';
import { getSession } from '~/lib/auth';
import { db, idParamsSchema } from '~/lib/db';
import { createElysia } from '~/util/elysia';
import { TodoForm, TodoItem, TodoList } from './components';

export const routes = createElysia({ prefix: '/todos' })
.get('/', async (ctx) => {
const session = await getSession(ctx.request);

const allTodos = await db.select().from(todos).all();
return (
<div class='flex flex-col'>
<TodoForm enabled={!!session} />
<TodoList todos={allTodos} enabled={!!session} />
<TodoForm enabled={!!ctx.session} />
<TodoList todos={allTodos} enabled={!!ctx.session} />
</div>
);
})
Expand All @@ -26,7 +23,7 @@ export const routes = createElysia({ prefix: '/todos' })
},
{
body: insertTodoSchema,
beforeHandle: [isAuthenticated],
// beforeHandle: [isAuthenticated],
}
)
.patch(
Expand All @@ -43,7 +40,7 @@ export const routes = createElysia({ prefix: '/todos' })
{
body: patchTodoSchema,
params: idParamsSchema,
beforeHandle: [isAuthenticated],
// beforeHandle: [isAuthenticated],
}
)
.delete(
Expand All @@ -53,6 +50,6 @@ export const routes = createElysia({ prefix: '/todos' })
},
{
params: idParamsSchema,
beforeHandle: [isAuthenticated],
// beforeHandle: [isAuthenticated],
}
);
39 changes: 13 additions & 26 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
import type * as elements from 'typed-html';

type LayoutProps = {
title?: string;
};

export const Layout = (props: LayoutProps & elements.Children) => {
const { title = 'elysia-kickstart' } = props;
export const Layout: Html.Component<LayoutProps> = ({ title, children }) => {
return (
'<!DOCTYPE html>' +
(
<html lang='en' class='dark'>
<head>
<meta charset='UTF-8' />
<meta
name='viewport'
content='width=device-width, initial-scale=1.0'
/>
<link
rel='icon'
type='image/x-icon'
href='/public/favicon.ico'
></link>
<title>{title}</title>
<link href='/public/globals.css' rel='stylesheet' />
<script src='/public/[email protected]'></script>
<script src='/public/use-theme.js' />
</head>
<body>{props.children}</body>
</html>
)
<html lang='en' class='dark'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<link rel='icon' type='image/x-icon' href='/public/favicon.ico'></link>
<title>{title ?? 'elysia-kickstart'}</title>
<link href='/public/globals.css' rel='stylesheet' />
<script src='/public/[email protected]'></script>
<script src='/public/use-theme.js' />
</head>
<body>{children}</body>
</html>
);
};
Loading
Loading