├── .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 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 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 | 2 | 3 | 9 | 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 | 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 |
15 | 16 |
17 |
18 | ) : ( 19 |
20 | Login 21 |
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 | --------------------------------------------------------------------------------