Public
Script
Readme

Lastlogin Authentication for val.town

Looking for an hono integration ? See @pomdtr/lastloginHono

Support login in trough:

  • Email Link
  • QR Code
  • Google Oauth
  • Github Oauth
  • Gitlab Oauth
  • Facebook Oauth

Demo

You can try a demo at https://pomdtr-lastloginhonoexample.web.val.run (see @pomdtr/lastLoginHonoExample for code)

image.png

Usage

Wrap your http handlers in a lastlogin middleware (sessions will be persisted in the lastlogin_session table on your sqlite account).

If you want to be the only one able to access your val, you can use @pomdtr/verifyUserEmail.

import { lastlogin } from "https://esm.town/v/pomdtr/lastlogin"; import { verifyUserEmail } from "https://esm.town/v/pomdtr/verifyUserEmail"; export default lastlogin((req) => { return new Response(`You are logged in as ${req.headers.get("X-LastLogin-Email")}`); }, { // check that the user email match your val town email verifyEmail: verifyUserEmail });

If you want to customize how is allowed to signup, you can set the verifyEmail option:

import { lastlogin } from "https://esm.town/v/pomdtr/lastlogin"; export default lastlogin((req) => { return new Response(`You are logged in as ${req.headers.get("X-LastLogin-Email")}`); }, { verifyEmail: (email) => { email == "steve@valtown" } });

You can allow anyone to signup by returning a boolean from the verifyEmail function:

import { lastlogin } from "https://esm.town/v/pomdtr/lastlogin"; export default lastlogin((req) => { return new Response(`You are logged in as ${req.headers.get("X-LastLogin-Email")}`); }, { verifyEmail: (_email) => true });

Public Routes

import { lastlogin } from "https://esm.town/v/pomdtr/lastlogin"; import { verifyUserEmail } from "https://esm.town/v/pomdtr/verifyUserEmail"; export default lastlogin(() => { return new Response("Hi!"); }, { verifyEmail: verifyUserEmail, public_routes: ["/", "/public/*"], });

See the URLPattern API for reference.

Logout

Just redirect the user to /auth/logout

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 { api } from "https://esm.town/v/pomdtr/api";
import { zip } from "https://esm.town/v/pomdtr/sql";
import { sqlite } from "https://esm.town/v/std/sqlite";
import { deleteCookie, getCookies, setCookie } from "jsr:@std/http/cookie";
async function createSession(email: string, hostname: string) {
const sessionID = crypto.randomUUID();
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 14);
await sqlite.batch([
`CREATE TABLE IF NOT EXISTS lastlogin_session (email TEXT, sessionID TEXT, hostname TEXT, expiresAt INTEGER)`,
{
sql: `DELETE FROM lastlogin_session WHERE email=? & hostname=?`,
args: [email, hostname],
},
{
sql: `INSERT INTO lastlogin_session VALUES (?, ?, ?, ?)`,
args: [email, sessionID, hostname, expiresAt.getTime()],
},
]);
return sessionID;
}
async function getSession(sessionID: string, hostname: string) {
try {
const res = await sqlite.execute({
sql: `SELECT * FROM lastlogin_session WHERE sessionID=? AND hostname=?`,
args: [sessionID, hostname],
});
const rows = zip(res);
if (rows.length === 0) {
return null;
}
const session = rows[0];
if (session.expiresAt < Date.now()) {
await sqlite.execute({
sql: `DELETE FROM lastlogin_session WHERE sessionID=?`,
args: [sessionID],
});
return null;
}
return session;
} catch (_) {
return null;
}
}
async function deleteSession(sessionID: string) {
await sqlite.execute({
sql: `DELETE FROM lastlogin_session WHERE sessionID=?`,
args: [sessionID],
});
}
const SESSION_COOKIE = "lastlogin_session";
const OAUTH_COOKIE = "oauth_store";
export type LastLoginOptions = {
verifyEmail: (email: string) => Promise<boolean> | boolean;
public_routes?: string[];
};
export function lastlogin(
handler: (req: Request) => Response | Promise<Response>,
options: LastLoginOptions,
) {
return async (req: Request) => {
const isPublicRoute = () => {
for (const pathname of options.public_routes ?? []) {
const pattern = new URLPattern({ pathname });
if (pattern.test(req.url)) {
return true;
}
}
return false;
};
const url = new URL(req.url);
const clientID = `${url.protocol}//${url.host}/`;
const redirectUri = `${url.protocol}//${url.host}/auth/callback`;
if (url.pathname == "/auth/callback") {
const cookies = await getCookies(req.headers);
const store = JSON.parse(decodeURIComponent(cookies[OAUTH_COOKIE]));
const state = url.searchParams.get("state");
if (!state || state != store.state) {
return new Response("state mismatch", { status: 400 });
}
const code = url.searchParams.get("code");
if (!code) {
return new Response("code not found", { status: 400 });
}
const tokenUrl = new URL("https://lastlogin.io/token");
tokenUrl.searchParams.set("client_id", clientID);
tokenUrl.searchParams.set("code", code);
tokenUrl.searchParams.set("redirect_uri", redirectUri);
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
Nobody has commented on this val yet: be the first!
August 24, 2024