├── .gitignore
├── .vscode
├── extensions.json
└── launch.json
├── README.md
├── astro.config.mjs
├── migrations
├── 0000_loud_blackheart.sql
└── meta
│ ├── 0000_snapshot.json
│ └── _journal.json
├── package-lock.json
├── package.json
├── public
└── favicon.svg
├── src
├── components
│ └── Card.astro
├── db
│ └── schema.ts
├── env.d.ts
├── layouts
│ └── Layout.astro
├── lib
│ └── auth.ts
├── middleware.ts
└── pages
│ ├── api
│ └── logout.ts
│ ├── index.astro
│ └── login
│ ├── github
│ ├── callback.ts
│ └── index.ts
│ └── index.astro
├── tsconfig.json
└── wrangler.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # jetbrains setting folder
24 | .idea/
25 |
26 | # wrangler files
27 | .wrangler
28 | .dev.vars
29 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro Starter Kit: Basics
2 |
3 | ```sh
4 | npm create astro@latest -- --template basics
5 | ```
6 |
7 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
8 | [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
9 | [](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
10 |
11 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
12 |
13 | 
14 |
15 | ## 🚀 Project Structure
16 |
17 | Inside of your Astro project, you'll see the following folders and files:
18 |
19 | ```text
20 | /
21 | ├── public/
22 | │ └── favicon.svg
23 | ├── src/
24 | │ ├── components/
25 | │ │ └── Card.astro
26 | │ ├── layouts/
27 | │ │ └── Layout.astro
28 | │ └── pages/
29 | │ └── index.astro
30 | └── package.json
31 | ```
32 |
33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
34 |
35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
36 |
37 | Any static assets, like images, can be placed in the `public/` directory.
38 |
39 | ## 🧞 Commands
40 |
41 | All commands are run from the root of the project, from a terminal:
42 |
43 | | Command | Action |
44 | | :------------------------ | :----------------------------------------------- |
45 | | `npm install` | Installs dependencies |
46 | | `npm run dev` | Starts local dev server at `localhost:4321` |
47 | | `npm run build` | Build your production site to `./dist/` |
48 | | `npm run preview` | Preview your build locally, before deploying |
49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
50 | | `npm run astro -- --help` | Get help using the Astro CLI |
51 |
52 | ## 👀 Want to learn more?
53 |
54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
55 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config';
2 |
3 | import cloudflare from "@astrojs/cloudflare";
4 |
5 | // https://astro.build/config
6 | export default defineConfig({
7 | output: "server",
8 | adapter: cloudflare({
9 | platformProxy: {
10 | enabled: true
11 | }
12 | })
13 | });
--------------------------------------------------------------------------------
/migrations/0000_loud_blackheart.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `sessions` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `user_id` text NOT NULL,
4 | `expires_at` integer NOT NULL,
5 | FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
6 | );
7 | --> statement-breakpoint
8 | CREATE TABLE `users` (
9 | `id` text PRIMARY KEY NOT NULL,
10 | `github_id` integer,
11 | `username` text
12 | );
13 | --> statement-breakpoint
14 | CREATE UNIQUE INDEX `users_github_id_unique` ON `users` (`github_id`);
--------------------------------------------------------------------------------
/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "sqlite",
4 | "id": "f717866f-35ac-4cf1-b57c-acdbd4cf12c8",
5 | "prevId": "00000000-0000-0000-0000-000000000000",
6 | "tables": {
7 | "sessions": {
8 | "name": "sessions",
9 | "columns": {
10 | "id": {
11 | "name": "id",
12 | "type": "text",
13 | "primaryKey": true,
14 | "notNull": true,
15 | "autoincrement": false
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "autoincrement": false
23 | },
24 | "expires_at": {
25 | "name": "expires_at",
26 | "type": "integer",
27 | "primaryKey": false,
28 | "notNull": true,
29 | "autoincrement": false
30 | }
31 | },
32 | "indexes": {},
33 | "foreignKeys": {
34 | "sessions_user_id_users_id_fk": {
35 | "name": "sessions_user_id_users_id_fk",
36 | "tableFrom": "sessions",
37 | "tableTo": "users",
38 | "columnsFrom": [
39 | "user_id"
40 | ],
41 | "columnsTo": [
42 | "id"
43 | ],
44 | "onDelete": "no action",
45 | "onUpdate": "no action"
46 | }
47 | },
48 | "compositePrimaryKeys": {},
49 | "uniqueConstraints": {}
50 | },
51 | "users": {
52 | "name": "users",
53 | "columns": {
54 | "id": {
55 | "name": "id",
56 | "type": "text",
57 | "primaryKey": true,
58 | "notNull": true,
59 | "autoincrement": false
60 | },
61 | "github_id": {
62 | "name": "github_id",
63 | "type": "integer",
64 | "primaryKey": false,
65 | "notNull": false,
66 | "autoincrement": false
67 | },
68 | "username": {
69 | "name": "username",
70 | "type": "text",
71 | "primaryKey": false,
72 | "notNull": false,
73 | "autoincrement": false
74 | }
75 | },
76 | "indexes": {
77 | "users_github_id_unique": {
78 | "name": "users_github_id_unique",
79 | "columns": [
80 | "github_id"
81 | ],
82 | "isUnique": true
83 | }
84 | },
85 | "foreignKeys": {},
86 | "compositePrimaryKeys": {},
87 | "uniqueConstraints": {}
88 | }
89 | },
90 | "enums": {},
91 | "_meta": {
92 | "schemas": {},
93 | "tables": {},
94 | "columns": {}
95 | }
96 | }
--------------------------------------------------------------------------------
/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "sqlite",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "5",
8 | "when": 1714837087488,
9 | "tag": "0000_loud_blackheart",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-cloudflare-pages",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro build && wrangler pages dev",
10 | "astro": "astro",
11 | "deploy": "astro build && wrangler pages deploy",
12 | "cf-typegen": "wrangler types"
13 | },
14 | "dependencies": {
15 | "@astrojs/check": "^0.5.10",
16 | "@astrojs/cloudflare": "^10.2.5",
17 | "@lucia-auth/adapter-drizzle": "^1.0.7",
18 | "arctic": "^1.8.0",
19 | "astro": "^4.7.1",
20 | "drizzle-kit": "^0.20.17",
21 | "drizzle-orm": "^0.30.10",
22 | "lucia": "^3.2.0",
23 | "typescript": "^5.4.5"
24 | },
25 | "devDependencies": {
26 | "@cloudflare/workers-types": "^4.20240502.0",
27 | "wrangler": "^3.53.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/components/Card.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | body: string;
5 | href: string;
6 | }
7 |
8 | const { href, title, body } = Astro.props;
9 | ---
10 |
11 |
12 |
13 |
14 | {title}
15 | →
16 |
17 |
18 | {body}
19 |
20 |
21 |
22 |
62 |
--------------------------------------------------------------------------------
/src/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2 |
3 | export const userTable = sqliteTable("users", {
4 | id: text("id").notNull().primaryKey(),
5 | githubId: integer("github_id").unique(),
6 | username: text("username"),
7 | });
8 |
9 | export const sessionTable = sqliteTable("sessions", {
10 | id: text("id").notNull().primaryKey(),
11 | userId: text("user_id")
12 | .notNull()
13 | .references(() => userTable.id),
14 | expiresAt: integer("expires_at").notNull(),
15 | });
16 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | type D1Database = import("@cloudflare/workers-types").D1Database;
5 | type Env = {
6 | DB: D1Database;
7 | GITHUB_CLIENT_ID: string;
8 | GITHUB_CLIENT_SECRET: string;
9 | };
10 | type Runtime = import("@astrojs/cloudflare").Runtime;
11 | declare namespace App {
12 | interface Locals extends Runtime {
13 | session: import("lucia").Session | null;
14 | user: import("lucia").User | null;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | }
5 |
6 | const { title } = Astro.props;
7 | ---
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {title}
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { Lucia } from "lucia";
2 | import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
3 | import { drizzle } from "drizzle-orm/d1";
4 | import { sessionTable, userTable } from "@db/schema";
5 |
6 | import { GitHub } from "arctic";
7 |
8 | export function initializeGithubClient(env: Env) {
9 | return new GitHub(env.GITHUB_CLIENT_ID, env.GITHUB_CLIENT_SECRET);
10 | }
11 |
12 | export function initializeLucia(D1: D1Database) {
13 | const db = drizzle(D1);
14 | const adapter = new DrizzleSQLiteAdapter(db, sessionTable, userTable);
15 | return new Lucia(adapter, {
16 | sessionCookie: {
17 | attributes: {
18 | secure: false,
19 | },
20 | },
21 | getUserAttributes: (attributes) => {
22 | return {
23 | githubId: attributes.githubId,
24 | username: attributes.username,
25 | };
26 | },
27 | });
28 | }
29 |
30 | interface DatabaseUserAttributes {
31 | githubId: number;
32 | username: string;
33 | }
34 |
35 | declare module "lucia" {
36 | interface Register {
37 | Lucia: ReturnType;
38 | DatabaseUserAttributes: DatabaseUserAttributes;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { initializeLucia } from "@lib/auth";
2 | import { verifyRequestOrigin } from "lucia";
3 | import { defineMiddleware } from "astro:middleware";
4 |
5 | export const onRequest = defineMiddleware(async (context, next) => {
6 | if (context.request.method !== "GET") {
7 | const originHeader = context.request.headers.get("Origin");
8 | const hostHeader = context.request.headers.get("Host");
9 | if (
10 | !originHeader ||
11 | !hostHeader ||
12 | !verifyRequestOrigin(originHeader, [hostHeader])
13 | ) {
14 | return new Response(null, {
15 | status: 403,
16 | });
17 | }
18 | }
19 |
20 | const lucia = initializeLucia(context.locals.runtime.env.DB);
21 |
22 | const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null;
23 | if (!sessionId) {
24 | context.locals.user = null;
25 | context.locals.session = null;
26 | return next();
27 | }
28 |
29 | const { session, user } = await lucia.validateSession(sessionId);
30 | if (session && session.fresh) {
31 | const sessionCookie = lucia.createSessionCookie(session.id);
32 | context.cookies.set(
33 | sessionCookie.name,
34 | sessionCookie.value,
35 | sessionCookie.attributes,
36 | );
37 | }
38 | if (!session) {
39 | const sessionCookie = lucia.createBlankSessionCookie();
40 | context.cookies.set(
41 | sessionCookie.name,
42 | sessionCookie.value,
43 | sessionCookie.attributes,
44 | );
45 | }
46 | context.locals.session = session;
47 | context.locals.user = user;
48 | return next();
49 | });
50 |
--------------------------------------------------------------------------------
/src/pages/api/logout.ts:
--------------------------------------------------------------------------------
1 | import { initializeLucia } from "@lib/auth";
2 | import type { APIContext } from "astro";
3 |
4 | export async function POST(context: APIContext): Promise {
5 | if (!context.locals.session) {
6 | return new Response(null, {
7 | status: 401,
8 | });
9 | }
10 |
11 | const lucia = initializeLucia(context.locals.runtime.env.DB);
12 |
13 | await lucia.invalidateSession(context.locals.session.id);
14 |
15 | const sessionCookie = lucia.createBlankSessionCookie();
16 | context.cookies.set(
17 | sessionCookie.name,
18 | sessionCookie.value,
19 | sessionCookie.attributes,
20 | );
21 |
22 | return context.redirect("/login");
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from "../layouts/Layout.astro";
3 | import Card from "../components/Card.astro";
4 |
5 | const { user } = Astro.locals;
6 | ---
7 |
8 |
9 |
10 | {
11 | user ? (
12 |
13 |
{JSON.stringify(user)}
14 |
17 |
18 | ) : (
19 |
22 | )
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/pages/login/github/callback.ts:
--------------------------------------------------------------------------------
1 | import { initializeLucia, initializeGithubClient } from "@lib/auth";
2 | import { OAuth2RequestError } from "arctic";
3 | import { generateIdFromEntropySize } from "lucia";
4 | import { drizzle } from "drizzle-orm/d1";
5 | import { eq } from "drizzle-orm";
6 | import * as schema from "@db/schema";
7 |
8 | import type { APIContext } from "astro";
9 |
10 | export async function GET(context: APIContext): Promise {
11 | const github = initializeGithubClient(context.locals.runtime.env);
12 | const lucia = initializeLucia(context.locals.runtime.env.DB);
13 | const db = drizzle(context.locals.runtime.env.DB, { schema });
14 | const code = context.url.searchParams.get("code");
15 | const state = context.url.searchParams.get("state");
16 | const storedState = context.cookies.get("github_oauth_state")?.value ?? null;
17 | if (!code || !state || !storedState || state !== storedState) {
18 | return new Response(null, {
19 | status: 400,
20 | });
21 | }
22 |
23 | try {
24 | const tokens = await github.validateAuthorizationCode(code);
25 | const githubUserResponse = await fetch("https://api.github.com/user", {
26 | headers: {
27 | Authorization: `Bearer ${tokens.accessToken}`,
28 | "User-Agent": "astro-cloudflare-pages",
29 | },
30 | });
31 | const githubUser: GitHubUser = await githubUserResponse.json();
32 |
33 | const existingUser = await db.query.userTable.findFirst({
34 | where: eq(schema.userTable.githubId, parseInt(githubUser.id)),
35 | });
36 |
37 | if (existingUser) {
38 | const session = await lucia.createSession(existingUser.id, {});
39 | const sessionCookie = lucia.createSessionCookie(session.id);
40 | context.cookies.set(
41 | sessionCookie.name,
42 | sessionCookie.value,
43 | sessionCookie.attributes,
44 | );
45 | return context.redirect("/");
46 | }
47 |
48 | const userId = generateIdFromEntropySize(10);
49 |
50 | await db.insert(schema.userTable).values({
51 | id: userId,
52 | githubId: parseInt(githubUser.id),
53 | username: githubUser.login,
54 | });
55 |
56 | const session = await lucia.createSession(userId, {});
57 | const sessionCookie = lucia.createSessionCookie(session.id);
58 | context.cookies.set(
59 | sessionCookie.name,
60 | sessionCookie.value,
61 | sessionCookie.attributes,
62 | );
63 | return context.redirect("/");
64 | } catch (e) {
65 | // the specific error message depends on the provider
66 | if (e instanceof OAuth2RequestError) {
67 | // invalid code
68 | return new Response(null, {
69 | status: 400,
70 | });
71 | }
72 | return new Response(null, {
73 | status: 500,
74 | });
75 | }
76 | }
77 |
78 | interface GitHubUser {
79 | id: string;
80 | login: string;
81 | }
82 |
--------------------------------------------------------------------------------
/src/pages/login/github/index.ts:
--------------------------------------------------------------------------------
1 | import { generateState } from "arctic";
2 | import { initializeGithubClient } from "@lib/auth";
3 |
4 | import type { APIContext } from "astro";
5 |
6 | export async function GET(context: APIContext): Promise {
7 | const github = initializeGithubClient(context.locals.runtime.env);
8 | const state = generateState();
9 | const url = await github.createAuthorizationURL(state);
10 |
11 | context.cookies.set("github_oauth_state", state, {
12 | path: "/",
13 | secure: import.meta.env.PROD,
14 | httpOnly: true,
15 | maxAge: 60 * 10,
16 | sameSite: "lax",
17 | });
18 |
19 | return context.redirect(url.toString());
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/login/index.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sign in
4 | Sign in with GitHub
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "types": ["@cloudflare/workers-types/2023-07-01"],
5 | "baseUrl": ".",
6 | "paths": {
7 | "@components/*": ["src/components/*"],
8 | "@lib/*": ["src/lib/*"],
9 | "@db/*": ["src/db/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | #:schema node_modules/wrangler/config-schema.json
2 | name = "astro-cloudflare-pages"
3 | compatibility_date = "2024-05-02"
4 | pages_build_output_dir = "./dist"
5 |
6 | [[d1_databases]]
7 | binding = "DB"
8 | database_name = "astro-cloudflare-pages"
9 | database_id = "3e5f8399-ea8e-4f94-b62d-00f610f81e1a"
10 |
--------------------------------------------------------------------------------