Skip to content

Commit 0be5dd0

Browse files
committed
Include installation README
1 parent 2572139 commit 0be5dd0

File tree

8 files changed

+174
-88
lines changed

8 files changed

+174
-88
lines changed

src/api/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This file is a template for adding your own API endpoints.
2+
// You can access these endpoints at the following URL:
3+
// https://starbasedb.YOUR-IDENTIFIER.workers.dev/api/your/path/here
4+
5+
export async function handleApiRequest(request: Request): Promise<Response> {
6+
const url = new URL(request.url);
7+
8+
// EXAMPLE:
9+
// if (request.method === 'GET' && url.pathname === '/api/your/path/here') {
10+
// return new Response('Success', { status: 200 });
11+
// }
12+
13+
return new Response('Not found', { status: 404 });
14+
}

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { exportTableToJsonRoute } from './export/json';
88
import { exportTableToCsvRoute } from './export/csv';
99
import { importDumpRoute } from './import/dump';
1010
import { importTableFromJsonRoute } from './import/json';
11+
import { handleApiRequest } from "./api";
1112

1213
const DURABLE_OBJECT_ID = 'sql-durable-object';
1314

@@ -16,6 +17,7 @@ interface Env {
1617
DATABASE_DURABLE_OBJECT: DurableObjectNamespace;
1718
STUDIO_USER?: string;
1819
STUDIO_PASS?: string;
20+
// ## DO NOT REMOVE: INSERT TEMPLATE INTERFACE ##
1921
AUTH: {
2022
handleAuth(pathname: string, verb: string, body: any): Promise<Response>;
2123
}
@@ -189,6 +191,8 @@ export class DatabaseDurableObject extends DurableObject {
189191
return createResponse(undefined, 'Table name is required', 400);
190192
}
191193
return importTableFromJsonRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName, request);
194+
} else if (url.pathname.startsWith('/api')) {
195+
return await handleApiRequest(request);
192196
} else {
193197
return createResponse(undefined, 'Unknown operation', 400);
194198
}
@@ -289,6 +293,8 @@ export default {
289293
let id: DurableObjectId = env.DATABASE_DURABLE_OBJECT.idFromName(DURABLE_OBJECT_ID);
290294
let stub = env.DATABASE_DURABLE_OBJECT.get(id);
291295

296+
// ## DO NOT REMOVE: INSERT TEMPLATE ROUTING LOGIC ##
297+
292298
/**
293299
* If the pathname starts with /auth, we want to pass the request off to another Worker
294300
* that is responsible for handling authentication.

templates/auth/src/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Installation Guide
2+
Follow the below steps to deploy the user authentication template into your existing
3+
StarbaseDB instance. These steps will alter your StarbaseDB application logic so that
4+
it includes capabilities for handling the routing of `/auth` routes to a new Cloudflare
5+
Worker instance that will be deployed – which will handle all application logic for
6+
user authentication.
7+
8+
## Step-by-step Instructions
9+
10+
### Add service bindings to your StarbaseDB wrangler.toml
11+
This will let your StarbaseDB instance know that we are deploying another Worker
12+
and it should use that for our authentication application routing logic.
13+
14+
```
15+
[[services]]
16+
binding = "AUTH"
17+
service = "starbasedb_auth"
18+
entrypoint = "AuthEntrypoint"
19+
```
20+
21+
### Add AUTH to Env interface in `./src/index.ts`
22+
Updates your `./src/index.ts` inside your StarbaseDB project so that your project
23+
can now have a proper type-safe way of calling functionality that exists in this
24+
new Cloudflare Worker that handles authentication.
25+
26+
```
27+
AUTH: {
28+
handleAuth(pathname: string, verb: string, body: any): Promise<Response>;
29+
}
30+
```
31+
32+
### Add routing logic in default export in `./src/index.ts`
33+
We will add the below block of code in our `export default` section of our
34+
StarbaseDB so that we can pick up on any `/auth` routes and immediately redirect
35+
them to the new Cloudflare Worker.
36+
37+
```
38+
if (pathname.startsWith('/auth')) {
39+
const body = await request.json();
40+
return await env.AUTH.handleAuth(pathname, request.method, body);
41+
}
42+
```
43+
44+
### Execute SQL statements in `migration.sql` to create required tables
45+
This will create the tables and constraints for user signup/login, and sessions
46+
required for the authentication operations to succeed.
47+
48+
### Run typegen in main project
49+
With our newly added service bindings in our StarbaseDB `wrangler.toml` file we can
50+
now generate an updated typegen output so our project knows that `AUTH` exists.
51+
```
52+
npm run cf-typegen
53+
```
54+
55+
### Deploy template project to Cloudflare
56+
Next, we will deploy our new authentication logic to a new Cloudflare Worker instance.
57+
```
58+
cd ./templates/auth
59+
npm run deploy
60+
```
61+
62+
### Deploy updates in our main StarbaseDB
63+
With all of the changes we have made to our StarbaseDB instance we can now deploy
64+
the updates so that all of the new authentication application logic can exist and
65+
be accessible.

templates/auth/src/email/index.ts

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,60 @@
11
import { createResponse, encryptPassword, verifyPassword } from "../utils";
22

33
export async function signup(stub: any, env: any, body: any) {
4-
if ((!body.email && !body.username) || !body.password) {
5-
return new Response(JSON.stringify({error: "Missing required fields"}), {status: 400, headers: {'Content-Type': 'application/json'}});
6-
}
4+
try {
5+
if ((!body.email && !body.username) || !body.password) {
6+
return createResponse(undefined, "Missing required fields", 400);
7+
}
78

8-
const isValidPassword = verifyPassword(env, body.password);
9-
if (!isValidPassword) {
10-
const errorMessage = `Password must be at least ${env.PASSWORD_REQUIRE_LENGTH} characters, ` +
11-
`${env.PASSWORD_REQUIRE_UPPERCASE ? "contain at least one uppercase letter, " : ""}` +
12-
`${env.PASSWORD_REQUIRE_LOWERCASE ? "contain at least one lowercase letter, " : ""}` +
13-
`${env.PASSWORD_REQUIRE_NUMBER ? "contain at least one number, " : ""}` +
14-
`${env.PASSWORD_REQUIRE_SPECIAL ? "contain at least one special character, " : ""}`;
15-
return createResponse(undefined, errorMessage, 400);
16-
}
9+
const isValidPassword = verifyPassword(env, body.password);
10+
if (!isValidPassword) {
11+
const errorMessage = `Password must be at least ${env.PASSWORD_REQUIRE_LENGTH} characters ` +
12+
`${env.PASSWORD_REQUIRE_UPPERCASE ? ", contain at least one uppercase letter " : ""}` +
13+
`${env.PASSWORD_REQUIRE_LOWERCASE ? ", contain at least one lowercase letter " : ""}` +
14+
`${env.PASSWORD_REQUIRE_NUMBER ? ", contain at least one number " : ""}` +
15+
`${env.PASSWORD_REQUIRE_SPECIAL ? ", contain at least one special character " : ""}`;
16+
return createResponse(undefined, errorMessage, 400);
17+
}
1718

18-
// Check to see if the username or email already exists
19-
let verifyUserResponse = await stub.executeExternalQuery(`SELECT * FROM auth_users WHERE username = ? OR email = ?`, [body.username, body.email]);
20-
if (verifyUserResponse.result.length > 0) {
21-
return createResponse(undefined, "Username or email already exists", 400);
22-
}
19+
// Check to see if the username or email already exists
20+
let verifyUserResponse = await stub.executeExternalQuery(`SELECT * FROM auth_users WHERE username = ? OR email = ?`, [body.username, body.email]);
21+
if (verifyUserResponse?.result?.length > 0) {
22+
return createResponse(undefined, "Username or email already exists", 400);
23+
}
2324

24-
// Create the user
25-
const encryptedPassword = await encryptPassword(body.password);
26-
let createUserResponse = await stub.executeExternalQuery(
27-
`INSERT INTO auth_users (username, password, email)
28-
VALUES (?, ?, ?)
29-
RETURNING id, username, email`,
30-
[body.username, encryptedPassword, body.email]
31-
);
25+
// Create the user
26+
const encryptedPassword = await encryptPassword(body.password);
27+
let createUserResponse = await stub.executeExternalQuery(
28+
`INSERT INTO auth_users (username, password, email)
29+
VALUES (?, ?, ?)
30+
RETURNING id, username, email`,
31+
[body.username, encryptedPassword, body.email]
32+
);
3233

33-
if (createUserResponse.result.length === 0) {
34-
return createResponse(undefined, "Failed to create user", 500);
35-
}
34+
console.log('Flag 6')
35+
if (createUserResponse?.result?.length === 0) {
36+
return createResponse(undefined, "Failed to create user", 500);
37+
}
3638

37-
// Create a session for the user
38-
const sessionToken = crypto.randomUUID();
39-
let createSessionResponse = await stub.executeExternalQuery(
40-
`INSERT INTO auth_sessions (user_id, session_token)
41-
VALUES (?, ?)
42-
RETURNING user_id, session_token, created_at`,
43-
[createUserResponse.result[0].id, sessionToken]
44-
);
39+
console.log('Flag 7')
40+
// Create a session for the user
41+
const sessionToken = crypto.randomUUID();
42+
let createSessionResponse = await stub.executeExternalQuery(
43+
`INSERT INTO auth_sessions (user_id, session_token)
44+
VALUES (?, ?)
45+
RETURNING user_id, session_token, created_at`,
46+
[createUserResponse.result[0].id, sessionToken]
47+
);
4548

46-
if (createSessionResponse.result.length === 0) {
47-
return createResponse(undefined, "Failed to create session", 500);
48-
}
49+
if (createSessionResponse?.result?.length === 0) {
50+
return createResponse(undefined, "Failed to create session", 500);
51+
}
4952

50-
return createResponse(createSessionResponse.result[0], undefined, 200);
53+
return createResponse(createSessionResponse.result[0], undefined, 200);
54+
} catch (error) {
55+
console.error('Signup Error:', error);
56+
return createResponse(undefined, "Username or email already exists", 500);
57+
}
5158
}
5259

5360
export async function login(stub: any, body: any) {
@@ -57,7 +64,7 @@ export async function login(stub: any, body: any) {
5764

5865
const encryptedPassword = await encryptPassword(body.password);
5966
let verifyUserResponse = await stub.executeExternalQuery(`SELECT * FROM auth_users WHERE (username = ? OR email = ?) AND password = ?`, [body.username, body.email, encryptedPassword]);
60-
if (verifyUserResponse.result.length === 0) {
67+
if (verifyUserResponse?.result?.length === 0) {
6168
return createResponse(undefined, "User not found", 404);
6269
}
6370

@@ -72,7 +79,7 @@ export async function login(stub: any, body: any) {
7279
[user.id, sessionToken]
7380
);
7481

75-
if (createSessionResponse.result.length === 0) {
82+
if (createSessionResponse?.result?.length === 0) {
7683
return createResponse(undefined, "Failed to create session", 500);
7784
}
7885

templates/auth/src/index.ts

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,6 @@ export default class AuthEntrypoint extends WorkerEntrypoint {
1010
// Currently, entrypoints without a named handler are not supported
1111
async fetch() { return new Response(null, {status: 404}); }
1212

13-
/**
14-
* Sets up the auth tables if they don't exist
15-
* @returns
16-
*/
17-
async setupAuthTables() {
18-
const createUserTableQuery = `
19-
CREATE TABLE IF NOT EXISTS auth_users (
20-
id INTEGER PRIMARY KEY AUTOINCREMENT,
21-
username TEXT UNIQUE,
22-
password TEXT NOT NULL,
23-
email TEXT UNIQUE,
24-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
25-
deleted_at TIMESTAMP DEFAULT NULL,
26-
email_confirmed_at TIMESTAMP DEFAULT NULL,
27-
CHECK ((username IS NOT NULL AND email IS NULL) OR (username IS NULL AND email IS NOT NULL) OR (username IS NOT NULL AND email IS NOT NULL))
28-
);
29-
`;
30-
31-
const createSessionTableQuery = `
32-
CREATE TABLE IF NOT EXISTS auth_sessions (
33-
id INTEGER PRIMARY KEY AUTOINCREMENT,
34-
user_id INTEGER NOT NULL,
35-
session_token TEXT NOT NULL UNIQUE,
36-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
37-
deleted_at TIMESTAMP DEFAULT NULL,
38-
FOREIGN KEY (user_id) REFERENCES auth_users (id)
39-
);
40-
`;
41-
42-
// Make a request to the binded database
43-
let response = await this.stub.executeExternalQuery(`${createUserTableQuery} ${createSessionTableQuery}`, []);
44-
return response;
45-
}
46-
4713
/**
4814
* Handles the auth requests, forwards to the appropriate handler
4915
* @param pathname
@@ -52,17 +18,15 @@ export default class AuthEntrypoint extends WorkerEntrypoint {
5218
* @returns
5319
*/
5420
async handleAuth(pathname: string, verb: string, body: any) {
55-
console.log('Handling Auth in Service Binding: ', body)
56-
5721
let id: DurableObjectId = this.env.DATABASE_DURABLE_OBJECT.idFromName(DURABLE_OBJECT_ID);
5822
this.stub = this.env.DATABASE_DURABLE_OBJECT.get(id);
5923

60-
await this.setupAuthTables();
61-
6224
if (verb === "POST" && pathname === "/auth/signup") {
63-
return emailSignup(this.stub, this.env, body);
25+
return await emailSignup(this.stub, this.env, body);
6426
} else if (verb === "POST" && pathname === "/auth/login") {
65-
return emailLogin(this.stub, body);
27+
return await emailLogin(this.stub, body);
28+
} else if (verb === "POST" && pathname === "/auth/logout") {
29+
return await this.handleLogout(body);
6630
}
6731

6832
return new Response(null, {status: 405});
@@ -74,7 +38,7 @@ export default class AuthEntrypoint extends WorkerEntrypoint {
7438
* @param body
7539
* @returns
7640
*/
77-
async handleLogout(request: Request, body: any) {
41+
async handleLogout(body: any) {
7842
await this.stub.executeExternalQuery(`UPDATE auth_sessions SET deleted_at = CURRENT_TIMESTAMP WHERE user_id = ?`, [body.user_id]);
7943
return createResponse(JSON.stringify({
8044
success: true,

templates/auth/src/migration.sql

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
CREATE TABLE IF NOT EXISTS auth_users (
2+
id INTEGER PRIMARY KEY AUTOINCREMENT,
3+
username TEXT COLLATE NOCASE,
4+
password TEXT NOT NULL,
5+
email TEXT COLLATE NOCASE,
6+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
7+
deleted_at TIMESTAMP DEFAULT NULL,
8+
email_confirmed_at TIMESTAMP DEFAULT NULL,
9+
UNIQUE(username),
10+
UNIQUE(email),
11+
CHECK ((username IS NOT NULL AND email IS NULL) OR (username IS NULL AND email IS NOT NULL) OR (username IS NOT NULL AND email IS NOT NULL))
12+
);
13+
14+
CREATE TRIGGER IF NOT EXISTS prevent_username_email_overlap
15+
BEFORE INSERT ON auth_users
16+
BEGIN
17+
SELECT CASE
18+
WHEN EXISTS (
19+
SELECT 1 FROM auth_users
20+
WHERE (NEW.username IS NOT NULL AND (NEW.username = username OR NEW.username = email))
21+
OR (NEW.email IS NOT NULL AND (NEW.email = username OR NEW.email = email))
22+
)
23+
THEN RAISE(ABORT, 'Username or email already exists')
24+
END;
25+
END;
26+
27+
CREATE TABLE IF NOT EXISTS auth_sessions (
28+
id INTEGER PRIMARY KEY AUTOINCREMENT,
29+
user_id INTEGER NOT NULL,
30+
session_token TEXT NOT NULL UNIQUE,
31+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
32+
deleted_at TIMESTAMP DEFAULT NULL,
33+
FOREIGN KEY (user_id) REFERENCES auth_users (id)
34+
);

templates/auth/wrangler.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ compatibility_date = "2024-09-25"
55
[durable_objects]
66
bindings = [{ name = "DATABASE_DURABLE_OBJECT", class_name = "DatabaseDurableObject", script_name = "starbasedb" }]
77

8-
# Allows for us to send user management emails such as email confirmation.
9-
send_email = [
10-
{name = "<NAME_FOR_BINDING1>"}
11-
]
12-
138
[vars]
149
REQUIRE_EMAIL_CONFIRM = 1
1510
PASSWORD_REQUIRE_LENGTH = 13

wrangler.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ compatibility_date = "2024-09-25"
55
account_id = ""
66

77
# Service Bindings
8+
## DO NOT REMOVE: INSERT TEMPLATE SERVICE BINDINGS ##
89
[[services]]
910
binding = "AUTH"
1011
service = "starbasedb_auth"

0 commit comments

Comments
 (0)