Readme

Email Auth for Val Town

⚠️ Require a pro account (needed to send email to users)

Usage

Create an http server, and wrap it in the emailAuth middleware.

Create valimport { emailAuth } from "https://www.val.town/v/pomdtr/email_auth" export default emailAuth((req, ctx) => { return new Response(`your mail is ${ctx.email}`); });

When an user access the val, he will need to input his mail, then confirm it through a confirmation code.

You can limit how can access your vals through an allowList:

Create valimport { emailAuth } from "https://www.val.town/v/pomdtr/email_auth" export default emailAuth((req, ctx) => { return new Response(`your mail is ${ctx.email}`); }, { allowList: ["steve@val.town"] });

If someone tries to access your val but is not in the allowlist, he will be blocked.

If you want to allow user to request for access, you can mix allowList with allowSignup:

Create valimport { emailAuth } from "https://www.val.town/v/pomdtr/email_auth" export default emailAuth((req, ctx) => { return new Response(`your mail is ${ctx.email}`); }, { allowList: ["steve@val.town"], allowSignup: true });

Each time a new user not present in the allowList try to login to a val, you will receive an email containing:

  • the email of the user trying to log in
  • the name of the val the he want to access

You can then just add the user to your whitelist to allow him in (and the user will not need to confirm his email again) !

Tips

If you don't want to put your email in clear text, you can just use an env variable:

Create valimport { emailAuth } from "https://www.val.town/v/pomdtr/email_auth" export default emailAuth((req, ctx) => { return new Response(`your mail is ${ctx.email}`); }, { allowList: [Deno.env.get("email")] });

Or just setup a forward val (see @pomdtr/inbox):

Create valimport { emailAuth } from "https://www.val.town/v/pomdtr/email_auth" export default emailAuth((req, ctx) => { return new Response(`your mail is ${ctx.email}`); }, { allowList: ["pomdtr.inbox@valtown.email"] });

TODO

  • Add expiration for verification codes and session tokens
  • use links instead of code for verification
  • improve errors pages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { deleteCookie, getCookies, setCookie } from "https://deno.land/std/http/cookie.ts";
import { inferRequestVal } from "https://esm.town/v/andreterron/inferRequestVal?v=2";
import { email as sendEmail } from "https://esm.town/v/std/email?v=11";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";
import { zip } from "npm:lodash-es";
import { nanoid } from "npm:nanoid";
import { createDate, TimeSpan } from "npm:oslo";
import { alphabet, generateRandomString } from "npm:oslo/crypto";
type Session = {
id: string;
email: string;
expiresAt: number;
};
async function createSessionTable(sessionTableName: string) {
await sqlite.execute(`CREATE TABLE ${sessionTableName} (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
email STRING NOT NULL,
val_slug STRING NOT NULL
);`);
}
async function createCodeTable(tableName: string) {
await sqlite.execute(`CREATE TABLE ${tableName} (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
code STRING NOT NULL,
email STRING NOT NULL,
expires_at INTEGER NOT NULL
)`);
}
async function generateEmailVerificationCode(tableName, email: string): Promise<string> {
try {
await sqlite.execute({ sql: `DELETE FROM ${tableName} WHERE email = ?`, args: [email] });
const code = generateRandomString(8, alphabet("0-9"));
const expires_at = createDate(new TimeSpan(5, "m")); // 5 minutes
await sqlite.execute({
sql: `INSERT INTO ${tableName} (email, code, expires_at) VALUES (?, ?, ?)`,
args: [email, code, expires_at.getTime() / 1000],
});
return code;
} catch (e) {
if (e.message.includes("no such table")) {
await createCodeTable(tableName);
return generateEmailVerificationCode(tableName, email);
}
throw e;
}
}
async function createSession(tableName: string, valSlug: string, email: string): Promise<Session> {
try {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
const sessionID = nanoid();
await sqlite.execute({
sql: `INSERT INTO ${tableName} (id, val_slug, expires_at, email) VALUES (?, ?, ?, ?)`,
args: [sessionID, valSlug, expiresAt.getTime() / 1000, email],
});
return {
id: sessionID,
email,
expiresAt: expiresAt.getTime(),
};
} catch (e) {
if (e.message.includes("no such table")) {
await createSessionTable(tableName);
return createSession(tableName, valSlug, email);
}
throw e;
}
}
async function getSession(tableName: string, sessionID: string, valSlug: string): Promise<Session> {
try {
const { rows, columns } = await sqlite.execute({
sql: `SELECT * FROM ${tableName} WHERE id = ? AND val_slug = ?`,
args: [sessionID, valSlug],
});
if (rows.length == 0) {
return null;
}
return Object.fromEntries(zip(columns, rows.at(0))) as Session;
} catch (e) {
if (e.message.includes("no such table")) {
return null;
}
throw e;
}
}
👆 This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data — all from the browser.