Readme

Password Auth Middleware

Protect your vals behind a password. Use session cookies to persist authentication.

6ed0648ae8813e958dbe79468572cb52f578239c0fae55857a13660beebdc5fd.png

Demo

See @pomdtr/password_auth_test

Usage

If you want to use an api token to authenticate:

Create valimport { passwordAuth } from "https://esm.town/v/pomdtr/password_auth"; export default passwordAuth(() => { return new Response("OK"); });

Or if you prefer to use a string:

Create valimport { passwordAuth } from "https://esm.town/v/pomdtr/password_auth"; export default passwordAuth(() => { return new Response("OK"); }, { password: Deno.env.get("VAL_PASSWORD") });

You can also set multiple ones

Create valimport { passwordAuth } from "https://esm.town/v/pomdtr/password_auth"; export default passwordAuth(() => { return new Response("OK"); }, { password: [Deno.env.get("VAL_PASSWORD"), Deno.env.get("STEVE_PASSWORD")] });

Note that authenticating using your api token remain an option even after setting a password.

TODO

  • allow to authenticate using a val town token
  • add a way to send an email to ask a password from the val owner
  • automatically extend the session
  • automatically remove expired sessions

FAQ

How to sign out ?

Navigate to <your-site>/signout.

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 { 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";
type Session = {
id: string;
expiresAt: number;
};
async function createSessionTable(tableName: string) {
await sqlite.execute(`CREATE TABLE ${tableName} (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
val_slug STRING NOT NULL
);`);
}
async function createSession(tableName: string, valSlug: string): Promise<Session> {
try {
const expires_at = new Date();
expires_at.setDate(expires_at.getDate() + 7);
const session: Session = { id: nanoid(), expiresAt: expires_at.getTime() };
await sqlite.execute({
sql: `INSERT INTO ${tableName} (id, val_slug, expires_at) VALUES (?, ?, ?)`,
args: [session.id, valSlug, session.expiresAt],
});
return session;
} catch (e) {
if (e.message.includes("no such table")) {
await createSessionTable(tableName);
return createSession(tableName, valSlug);
}
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;
}
}
async function fetchUser(token: string): Promise<{ id: string }> {
const resp = await fetch("https://api.val.town/v1/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (resp.status !== 200) {
throw new Error("Could not fetch user");
}
return resp.json();
}
async function verifyApiToken(token: string) {
try {
const [currentUser, requestUser] = await Promise.all([fetchUser(Deno.env.get("valtown")), fetchUser(token)]);
return currentUser.id == requestUser.id;
} catch (_) {
return false;
}
}
const loginPage = (handle) =>
`<html>
<head>
<link rel="icon" href="https://fav.farm/🔒" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
</head>
<body style="display: flex; justify-content: center; align-items: center;">
<article>
<p>This val website is <a href="https://www.val.town/v/pomdtr/password_auth">protected by a password</a>.</p>
<p>If you are <a href="https://val.town/u/${handle}">@${handle}</a>, you can access it using an <a href="https://www.val.town/settings/api">API token</a>.</p>
<p>If not, you'll need to contact the author for access.</p>
<footer>
<form method="POST" style="margin-block-end: 0em;">
<fieldset role="group" style="margin-bottom: 0em;">
<input id="password" placeholder="Password" name="password" type="password" />
<input type="submit" value="Sign In"/>
👆 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.