From 4eb8247f90798a3803e39b9c3fc199917602d649 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Dec 2021 14:40:48 -0500 Subject: [PATCH 01/11] WIP #18 --- src/index.ts | 22 +++++------ src/models/docs.ts | 21 ----------- src/models/gist.ts | 39 +++++++++----------- src/models/session.ts | 83 +----------------------------------------- src/models/todolist.ts | 12 +++--- src/models/user.ts | 66 +++++++++++---------------------- src/routes/auth.ts | 54 --------------------------- src/routes/docs.ts | 10 +++-- src/routes/gists.ts | 51 ++++++++++++++++---------- src/routes/sessions.ts | 58 +++++++++++++++++++++++++++++ src/routes/todos.ts | 22 +++++------ src/utils/auth.ts | 14 +++++++ src/utils/cookies.ts | 29 --------------- src/utils/database.ts | 5 +++ src/utils/github.ts | 59 ------------------------------ src/utils/handler.ts | 4 +- wrangler.example.toml | 1 + 17 files changed, 186 insertions(+), 364 deletions(-) delete mode 100644 src/models/docs.ts delete mode 100644 src/routes/auth.ts create mode 100644 src/routes/sessions.ts create mode 100644 src/utils/auth.ts delete mode 100644 src/utils/cookies.ts delete mode 100644 src/utils/github.ts diff --git a/src/index.ts b/src/index.ts index a1ef46c..e9b0fa3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,8 @@ import { Router } from 'worktop'; import * as CORS from 'worktop/cors'; import * as Cache from 'worktop/cache'; import * as Gists from './routes/gists'; +import * as Sessions from './routes/sessions'; import * as Todos from './routes/todos'; -import * as Auth from './routes/auth'; import * as Docs from './routes/docs'; const API = new Router(); @@ -12,20 +12,20 @@ API.prepare = CORS.preflight({ maxage: 3600 }); -API.add('GET', '/auth/login', Auth.login); -API.add('GET', '/auth/callback', Auth.callback); -API.add('GET', '/auth/logout', Auth.logout); +API.add('POST', '/session', Sessions.create); +API.add('GET', '/session/:sessionid', Sessions.show); +API.add('DELETE', '/session/:sessionid', Sessions.destroy); API.add('GET', '/gists', Gists.list); API.add('POST', '/gists', Gists.create); -API.add('GET', '/gists/:uid', Gists.show); -API.add('PUT', '/gists/:uid', Gists.update); -API.add('DELETE', '/gists/:uid', Gists.destroy); +API.add('GET', '/gists/:gistid', Gists.show); +API.add('PUT', '/gists/:gistid', Gists.update); +API.add('DELETE', '/gists/:gistid', Gists.destroy); -API.add('GET', '/todos/:userid', Todos.list); -API.add('POST', '/todos/:userid', Todos.create); -API.add('PATCH', '/todos/:userid/:uid', Todos.update); -API.add('DELETE', '/todos/:userid/:uid', Todos.destroy); +API.add('GET', '/todos/:guestid', Todos.list); +API.add('POST', '/todos/:guestid', Todos.create); +API.add('PATCH', '/todos/:guestid/:todoid', Todos.update); +API.add('DELETE', '/todos/:guestid/:todoid', Todos.destroy); API.add('GET', '/docs/:project/:type', Docs.list); API.add('GET', '/docs/:project/:type/:slug', Docs.entry); diff --git a/src/models/docs.ts b/src/models/docs.ts deleted file mode 100644 index 476a56b..0000000 --- a/src/models/docs.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { KV } from "worktop/kv"; - -declare const DOCS: KV.Namespace; - -export function list( - project: string, - type: string, - version: string, - full: boolean -): Promise { - return DOCS.get(`${project}@${version}:${type}:${full ? "content" : "list"}`); -} - -export function entry( - project: string, - type: string, - slug: string, - version: string -): Promise { - return DOCS.get(`${project}@${version}:${type}:${slug}`); -} diff --git a/src/models/gist.ts b/src/models/gist.ts index 07edf0f..498944a 100644 --- a/src/models/gist.ts +++ b/src/models/gist.ts @@ -13,7 +13,7 @@ export interface File { export type GistID = UID<36>; export interface Gist { - uid: GistID; + gistid: GistID; userid: UserID; name: string; files: File[]; @@ -25,25 +25,25 @@ export interface Gist { export const toUID = () => keys.gen(36); /** Find a Gist by its public ID value */ -export async function lookup(uid: GistID) { - return database.get('gist', uid); +export async function lookup(gistid: GistID) { + return database.get('gist', gistid); } /** Create a new Gist for the User */ -export async function insert(input: Partial, user: User.User): Promise { +export async function insert(input: Partial, userid: User.UserID): Promise { const values: Gist = { // wait until have unique GistID - uid: await keys.until(toUID, lookup), + gistid: await keys.until(toUID, lookup), name: input.name || '', files: input.files || [], - userid: user.uid, + userid, created_at: Date.now() }; - await database.put('gist', values.uid, values); + await database.put('gist', values.gistid, values); // synchronize the owner's list of gists - await sync(values.userid, values); + await sync(userid, values); // return the new item return values; @@ -57,7 +57,7 @@ export async function update(values: Gist, changes: Gist): Promise { values.updated_at = Date.now(); // update the gist with its new values - await database.put('gist', values.uid, values); + await database.put('gist', values.gistid, values); // synchronize the owner's list of gists await sync(values.userid, values); @@ -67,24 +67,21 @@ export async function update(values: Gist, changes: Gist): Promise { } /** Destroy an existing Gist record */ -export async function destroy(item: Gist): Promise { +export async function destroy(gist: Gist) { // remove the gist record itself - await database.remove('gist', item.uid); + await database.remove('gist', gist.gistid); // synchronize the owner's list of gists - await sync(item.userid, item, true); - - // return the deleted item - return item; + await sync(gist.userid, gist, true); } /** Synchronize UserGists list for User/Owner */ export async function sync(userid: UserID, target: Gist | User.UserGist, isRemove = false) { const list = await User.gists(userid); - if (target && target.uid) { + if (target && target.gistid) { for (let i=0; i < list.length; i++) { - if (list[i].uid === target.uid) { + if (list[i].gistid === target.gistid) { list.splice(i, 1); break; } @@ -92,7 +89,7 @@ export async function sync(userid: UserID, target: Gist | User.UserGist, isRemov } isRemove || list.unshift({ - uid: target.uid, + gistid: target.gistid, name: target.name, updated_at: target.updated_at || (target as Gist).created_at, }); @@ -104,7 +101,7 @@ export async function sync(userid: UserID, target: Gist | User.UserGist, isRemov * Format a Gist for API response * @NOTE Matches existing `svelte.dev` Gist output */ -export function output(item: Gist) { - const { uid, name, userid, files } = item; - return { uid, owner:userid, name, files }; +export function output(gist: Gist) { + const { gistid, name, userid, files } = gist; + return { gistid, owner:userid, name, files }; } diff --git a/src/models/session.ts b/src/models/session.ts index 34aef06..bb3e177 100644 --- a/src/models/session.ts +++ b/src/models/session.ts @@ -1,89 +1,10 @@ -import * as User from './user'; -import { HttpError } from '../utils/error'; -import * as database from '../utils/database'; -import * as cookies from '../utils/cookies'; -import * as keys from '../utils/keys'; - -// types -import type { Handler } from 'worktop'; import type { UID } from 'worktop/utils'; -import type { ServerResponse } from 'worktop/response'; -import type { ServerRequest } from 'worktop/request'; import type { UserID } from './user'; export type SessionID = UID<32>; export interface Session { - uid: SessionID; + sessionid: SessionID; userid: UserID; expires: TIMESTAMP; -} - -/** Create new `SessionID` value */ -export const toUID = () => keys.gen(32); - -/** Find a Session by its public ID value */ -export async function lookup(uid: SessionID) { - return database.get('session', uid); -} - -/** Create a new Session for the User */ -export async function insert(user: User.User): Promise { - const values: Session = { - // wait until have unique GistID - uid: await keys.until(toUID, lookup), - userid: user.uid, - // convert to milliseconds - expires: Date.now() + cookies.EXPIRES * 1000 - }; - - await database.put('session', values.uid, values); - - // return the new item - return values; -} - -/** Destroy an existing Session document */ -export async function destroy(item: Session): Promise { - await database.remove('session', item.uid); - return item; // return the deleted item -} - -/** Parse the "Cookie" request header; attempt valid `Session` -> `User` lookup */ -export async function identify(req: ServerRequest, res: ServerResponse): Promise { - const cookie = req.headers.get('cookie'); - if (!cookie) throw new HttpError('Missing cookie header', 401); - - const sid = cookies.parse(cookie); - if (!sid) throw new HttpError('Invalid cookie value', 401); - - const session = await lookup(sid); - if (!session) throw new HttpError('Invalid cookie token', 401); - - if (Date.now() >= session.expires) { - await database.remove('session', session.uid); - throw new HttpError('Expired session', 401); - } - - const user = await User.lookup(session.userid); - if (!user || user.uid !== session.userid) { - throw new HttpError('Invalid session', 401); - } - - return user; -} - -export type AuthorizedRequest = ServerRequest & { user: User.User, session: Session }; -export type AuthorizedHandler = (req: AuthorizedRequest, res: ServerResponse) => Promise; - -/** - * Authentication Middleware - * Only run `handler` if authenticated / valid Cookie. - * Guarantees `User` document as `req.user` to `handler`, else error. - */ -export function authenticate(handler: AuthorizedHandler): Handler { - return async function (req, res) { - (req as AuthorizedRequest).user = await identify(req, res); - return handler(req as AuthorizedRequest, res); - }; -} +} \ No newline at end of file diff --git a/src/models/todolist.ts b/src/models/todolist.ts index 6a3aef4..8769189 100644 --- a/src/models/todolist.ts +++ b/src/models/todolist.ts @@ -10,7 +10,7 @@ export type TodoID = UID<36>; export type GuestID = string; export interface Todo { - uid: TodoID; + todoid: TodoID; created_at: TIMESTAMP; text: string; done: boolean; @@ -31,7 +31,7 @@ export async function insert(userid: GuestID, text: string) { const list = await lookup(userid) || []; const todo: Todo = { - uid: keys.gen(36), + todoid: keys.gen(36), created_at: Date.now(), text, done: false @@ -44,12 +44,12 @@ export async function insert(userid: GuestID, text: string) { return todo; } -export async function update(userid: GuestID, uid: TodoID, patch: { text?: string, done?: boolean }) { +export async function update(userid: GuestID, todoid: TodoID, patch: { text?: string, done?: boolean }) { const list = await lookup(userid); if (!list) return; for (const todo of list) { - if (todo.uid === uid) { + if (todo.todoid === todoid) { if ('text' in patch) { todo.text = patch.text as string; } @@ -65,12 +65,12 @@ export async function update(userid: GuestID, uid: TodoID, patch: { text?: strin } } -export async function destroy(userid: GuestID, uid: TodoID) { +export async function destroy(userid: GuestID, todoid: TodoID) { const list = await lookup(userid); let i = list.length; while (i--) { - if (list[i].uid === uid) { + if (list[i].todoid === todoid) { list.splice(i, 1); await sync(userid, list); diff --git a/src/models/user.ts b/src/models/user.ts index 374baf6..57dad2e 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -2,11 +2,11 @@ import * as database from '../utils/database'; import type { Gist } from './gist'; -export type UserID = number; +export type UserID = string; export interface User { /** GitHub ID */ - uid: UserID; + id: UserID; /** GitHub username */ username: string; /** First + Last */ @@ -15,57 +15,33 @@ export interface User { avatar: string; /** GitHub oAuth token */ token: GitHub.AccessToken; - created_at: TIMESTAMP; + created_at?: TIMESTAMP; updated_at?: TIMESTAMP; } // The `Gist` attributes saved for `owner` relationship -export type UserGist = Pick; - -/** Find a User by its public ID value */ -export async function lookup(uid: UserID) { - return database.get('user', uid); -} +export type UserGist = Pick; /** Get all Gists belonging to UserID */ -export function gists(uid: UserID): Promise { - return database.get('owner', uid).then(arr => arr || []); -} - -/** Create a new User record */ -export async function insert(profile: GitHub.User, accesstoken: GitHub.AccessToken): Promise { - const values: User = { - uid: profile.id, - username: profile.login, - name: profile.name, - avatar: profile.avatar_url, - created_at: Date.now(), - token: accesstoken, - }; - - await database.put('user', values.uid, values); - - // return the new item - return values; +export function gists(userid: UserID): Promise { + return database.get('owner', userid).then(arr => arr || []); } -/** Update an existing User document */ -export async function update(values: User, profile: GitHub.User, accesstoken: GitHub.AccessToken): Promise { - // update specific attrs - values.name = profile.name; - values.username = profile.login; - values.avatar = profile.avatar_url; - values.updated_at = Date.now(); - values.token = accesstoken; +export async function upsert(user: Omit, 'updated_at'>) { + let record; - await database.put('user', values.uid, values); + try { + record = await database.get('user', user.id); + } catch { + record = {}; + } - // return the new item - return values; -} + const now = Date.now(); -/** Format a User for public display */ -export function output(item: User) { - const { uid, username, name, avatar } = item; - return { uid, username, name, avatar }; -} + await database.put('user', user.id, { + created_at: now, + ...record, + ...user, + updated_at: now + }); +} \ No newline at end of file diff --git a/src/routes/auth.ts b/src/routes/auth.ts deleted file mode 100644 index 5bdab62..0000000 --- a/src/routes/auth.ts +++ /dev/null @@ -1,54 +0,0 @@ -import devalue from 'devalue'; -import { HttpError } from '../utils/error'; -import * as github from '../utils/github'; -import * as cookies from '../utils/cookies'; -import * as Session from '../models/session'; -import * as User from '../models/user'; -import { handler } from '../utils/handler'; - -import type { Handler } from 'worktop'; - -// GET /auth/login -export const login: Handler = function (req, res) { - const Location = github.authorize(); - return Response.redirect(Location, 302); -} - -// GET /auth/callback -export const callback: Handler = handler(async function (req, res) { - const code = req.query.get('code') || ''; - if (!code) throw new HttpError('Missing "code" parameter', 400); - - // Trade "code" for "access_token" - const { token, profile } = await github.exchange(code); - - let user = await User.lookup(profile.id); - - if (user) { - user = await User.update(user, profile, token); - } else { - user = await User.insert(profile, token); - } - - const session = await Session.insert(user); - - res.setHeader('Content-Type', 'text/html;charset=utf-8'); - res.setHeader('Set-Cookie', cookies.serialize(session.uid)); - - // sanitize output, hide token - const output = User.output(user); - - res.end(` - - `); -}) - -// GET /auth/logout -export const logout = handler(Session.authenticate(async (req, res) => { - await Session.destroy(req.session); - res.send(204); -})); diff --git a/src/routes/docs.ts b/src/routes/docs.ts index 36e3d82..7ef4b04 100644 --- a/src/routes/docs.ts +++ b/src/routes/docs.ts @@ -1,8 +1,10 @@ -import * as Docs from "../models/docs"; +import { handler } from "../utils/handler"; import type { Handler } from "worktop"; import type { Params } from "worktop/request"; -import { handler } from "../utils/handler"; +import type { KV } from "worktop/kv"; + +declare const DOCS: KV.Namespace; type ParamsDocsList = Params & { project: string; type: string }; type ParamsDocsEntry = Params & { project: string; type: string; slug: string }; @@ -17,7 +19,7 @@ export const list: Handler = handler(async (req, res) => { const version = req.query.get("version") || "latest"; const full = req.query.get("content") !== null; - const docs = await Docs.list(project, type, version, full); + const docs = await DOCS.get(`${project}@${version}:${type}:${full ? "content" : "list"}`); res.send(200, docs, headers); }); @@ -26,6 +28,6 @@ export const entry: Handler = handler(async (req, res) => { const { project, type, slug } = req.params; const version = req.query.get("version") || "latest"; - const entry = await Docs.entry(project, type, slug, version); + const entry = await DOCS.get(`${project}@${version}:${type}:${slug}`); res.send(200, entry, headers); }); diff --git a/src/routes/gists.ts b/src/routes/gists.ts index e300fd3..9b50ec1 100644 --- a/src/routes/gists.ts +++ b/src/routes/gists.ts @@ -1,20 +1,25 @@ import * as Gist from '../models/gist'; -import * as Session from '../models/session'; import * as User from '../models/user'; import { HttpError } from '../utils/error'; -import type { Handler } from 'worktop'; import type { GistID } from '../models/gist'; +import type { UserID } from '../models/user'; import { handler } from '../utils/handler'; +import { authenticate } from '../utils/auth'; -// GET /gists -export const list = handler(Session.authenticate(async (req, res) => { - // already transformed and sorted by recency - res.send(200, await User.gists(req.user.uid)); +// GET /gists?userid=userid +export const list = handler(authenticate(async (req, res) => { + const userid = req.query.get('userid') as UserID; + if (!userid) throw new HttpError('Missing userid', 400); + + res.send(200, await User.gists(userid)); })); -// POST /gists -export const create = handler(Session.authenticate(async (req, res) => { +// POST /gists?userid=userid +export const create = handler(authenticate(async (req, res) => { + const userid = req.query.get('userid') as UserID; + if (!userid) throw new HttpError('Missing userid', 400); + const input = await req.body>(); if (!input) throw new HttpError('Missing request body', 400); @@ -22,21 +27,24 @@ export const create = handler(Session.authenticate(async (req, res) => { const name = (input.name || '').trim(); const files = ([] as Gist.File[]).concat(input.files || []); - const item = await Gist.insert({ name, files }, req.user); + const item = await Gist.insert({ name, files }, userid); res.send(201, Gist.output(item)); })); -// GET /gists/:uid -export const show: Handler = handler(async (req, res) => { - const item = await Gist.lookup(req.params.uid as GistID); +// GET /gists/:gistid +export const show = handler(async (req, res) => { + const item = await Gist.lookup(req.params.gistid as GistID); res.send(200, Gist.output(item)); }); -// PUT /gists/:uid -export const update = handler(Session.authenticate(async (req, res) => { - const item = await Gist.lookup(req.params.uid as GistID); +// PUT /gists/:gistid +export const update = handler(authenticate(async (req, res) => { + const userid = req.query.get('userid') as UserID; + if (!userid) throw new HttpError('Missing userid', 400); - if (req.user.uid !== item.userid) { + const item = await Gist.lookup(req.params.gistid as GistID); + + if (userid !== item.userid) { throw new HttpError('Gist does not belong to you', 403); } @@ -48,11 +56,14 @@ export const update = handler(Session.authenticate(async (req, res) => { res.send(200, Gist.output(values)); })); -// DELETE /gists/:uid -export const destroy = handler(Session.authenticate(async (req, res) => { - const item = await Gist.lookup(req.params.uid as GistID); +// DELETE /gists/:gistid +export const destroy = handler(authenticate(async (req, res) => { + const userid = req.query.get('userid') as UserID; + if (!userid) throw new HttpError('Missing userid', 400); + + const item = await Gist.lookup(req.params.gistid as GistID); - if (req.user.uid !== item.userid) { + if (userid !== item.userid) { throw new HttpError('Gist does not belong to you', 403); } diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts new file mode 100644 index 0000000..6549625 --- /dev/null +++ b/src/routes/sessions.ts @@ -0,0 +1,58 @@ +import { handler } from '../utils/handler'; +import * as database from '../utils/database'; +import * as keys from '../utils/keys'; +import { SessionID } from '../models/session'; +import * as User from '../models/user'; +import { HttpError } from '../utils/error'; +import { authenticate } from '../utils/auth'; + +// Expires in 1 year (seconds) +const EXPIRES = 86400 * 365; + +const get_uid = () => keys.gen(32); +const lookup = async (sessionid: SessionID) => { + const exists = await database.has('session', sessionid); + return !exists; +} + +// POST /session +export const create = handler(authenticate(async (req, res) => { + const user = await req.body(); + if (!user) throw new HttpError('Missing body', 400); + + const sessionid: SessionID = await keys.until(get_uid, lookup); + const expires = Date.now() + EXPIRES * 1000; + + // create or update the user object + await User.upsert(user); + + // create a session + await database.put('session', sessionid, { + sessionid, + userid: user.id, + expires + }); + + res.send(200, { sessionid, expires }); +})); + +// GET /session/:sessionid +export const show = handler(authenticate(async (req, res) => { + const { userid } = await database.get('session', req.params.sessionid as SessionID); + const user = await database.get('user', userid); + + res.send(200, { + user: { + id: user.id, + name: user.name, + username: user.username, + avatar: user.avatar + } + }); +})); + +// DELETE /session/:sessionid +export const destroy = handler(authenticate(async (req, res) => { + await database.remove('session', req.params.sessionid as SessionID); + res.send(204); +})); \ No newline at end of file diff --git a/src/routes/todos.ts b/src/routes/todos.ts index 01cb7b6..65c94bb 100644 --- a/src/routes/todos.ts +++ b/src/routes/todos.ts @@ -6,40 +6,40 @@ import type { Params } from 'worktop/request'; import type { TodoID, GuestID } from '../models/todolist'; import { handler } from '../utils/handler'; -type ParamsUserID = Params & { userid: GuestID }; +type ParamsUserID = Params & { guestid: GuestID }; -// GET /todos/:userid +// GET /todos/:guestid export const list: Handler = handler(async (req, res) => { - const todos = await TodoList.lookup(req.params.userid); + const todos = await TodoList.lookup(req.params.guestid); res.send(200, todos); }); -// POST /gists/:userid +// POST /todos/:guestid export const create: Handler = handler(async (req, res) => { const input = await req.body<{ text: string }>(); if (!input) throw new HttpError('Missing request body', 400); - const todo = await TodoList.insert(req.params.userid, input.text); + const todo = await TodoList.insert(req.params.guestid, input.text); res.send(201, todo); }); -// PATCH /gists/:userid/:uid +// PATCH /todos/:guestid/:todoid export const update: Handler = handler(async (req, res) => { - const { userid, uid } = req.params; + const { guestid, todoid } = req.params; const input = await req.body<{ text?: string, done?: boolean }>(); if (!input) throw new HttpError('Missing request body', 400); - const todo = await TodoList.update(userid, uid as TodoID, input); + const todo = await TodoList.update(guestid, todoid as TodoID, input); res.send(200, todo); }); -// DELETE /gists/:userid/:uid +// DELETE /todos/:guestid/:todoid export const destroy: Handler = handler(async (req, res) => { - const { userid, uid } = req.params; + const { guestid, todoid } = req.params; - await TodoList.destroy(userid, uid as TodoID); + await TodoList.destroy(guestid, todoid as TodoID); res.send(200, {}); // TODO should be a 204, no? }); diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..e5f1ec3 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,14 @@ +import { Handler } from 'worktop'; +import { HttpError } from './error'; + +declare var SECRET: string; + +export function authenticate(handler: Handler): Handler { + return function (req, res) { + if (req.headers.get('authorization') !== `Basic ${SECRET}`) { + throw new HttpError('Unauthorized', 401); + } + + return handler(req, res); + }; +} \ No newline at end of file diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts deleted file mode 100644 index 68eb24f..0000000 --- a/src/utils/cookies.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as cookie from 'worktop/cookie'; - -import type { SessionID } from '../models/session'; - -export interface Cookie { - sid: SessionID; -} - -// Expires in 1 year (seconds) -export const EXPIRES = 86400 * 365; - -export function isCookie(item: Cookie | Record): item is Cookie { - return item && typeof item.sid === 'string'; -} - -export function parse(value: string): SessionID | false { - const item = cookie.parse(value); - return isCookie(item) && item.sid; -} - -export function serialize(value: SessionID | null): string { - return cookie.stringify('sid', value || '', { - path: '/', - domain: 'svelte.dev', - maxage: value ? EXPIRES : -1, - httponly: true, - secure: true, - }); -} diff --git a/src/utils/database.ts b/src/utils/database.ts index 9a71d8d..88331e0 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -25,6 +25,11 @@ export interface Models { user: User; } +export function has(type: K, uid: Identifiers[K]): Promise { + const keyname = keys.format(type, uid); + return DATAB.get(keyname).then(() => true, () => false); +} + export function get(type: K, uid: Identifiers[K]): Promise { const keyname = keys.format(type, uid); diff --git a/src/utils/github.ts b/src/utils/github.ts deleted file mode 100644 index 3c52a31..0000000 --- a/src/utils/github.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { HttpError } from './error'; - -const SELF_API = 'https://api.svelte.dev'; -const GITHUB_API = 'https://github.com/login/oauth'; - -// Defined as Worker secrets -declare var GITHUB_CLIENT_ID: string; -declare var GITHUB_CLIENT_SECRET: string; - -// Construct authorize URL location -export function authorize() { - const addr = new URL(`${GITHUB_API}/authorize`); - addr.searchParams.set('scope', 'read:user'); - addr.searchParams.set('client_id', GITHUB_CLIENT_ID); - addr.searchParams.set('redirect_uri', `${SELF_API}/auth/callback`); - return addr.href; -} - -// Trade a "code" for an "access_token" -export async function access_token(code: string): Promise { - const oauth = new URL(`${GITHUB_API}/access_token`); - oauth.searchParams.set('code', code); - oauth.searchParams.set('client_id', GITHUB_CLIENT_ID); - oauth.searchParams.set('client_secret', GITHUB_CLIENT_SECRET); - - const res = await fetch(oauth.href, { method: 'POST' }); - const values = await res.formData(); - - const token = values.get('access_token'); - if (token) return token as GitHub.AccessToken; - else throw new HttpError('OAuth failed', 400); -} - -export async function user(token: GitHub.AccessToken): Promise { - const res = await fetch('https://api.github.com/user', { - headers: { - 'User-Agent': 'svelte.dev', - 'Authorization': `token ${token}` - } - }); - - if (res.ok) { - return res.json() as Promise; - } else { - throw new HttpError('Authentication failed', 400); - } -} - -export interface Payload { - token: GitHub.AccessToken; - profile: GitHub.User; -} - -export async function exchange(code: string): Promise { - const token = await access_token(code); - const profile = await user(token); - - return { token, profile }; -} diff --git a/src/utils/handler.ts b/src/utils/handler.ts index 956a8ae..93ae3ec 100644 --- a/src/utils/handler.ts +++ b/src/utils/handler.ts @@ -9,9 +9,9 @@ export function handler(fn: Handler): Handler { const status = (err as HttpError).statusCode || 500; const message = (err as HttpError).message; - if (status >= 500) { + // if (status >= 500) { console.error((err as HttpError).stack); - } + // } res.send(status, { status, message }); } diff --git a/wrangler.example.toml b/wrangler.example.toml index 761034b..45bda8a 100644 --- a/wrangler.example.toml +++ b/wrangler.example.toml @@ -27,6 +27,7 @@ upload.format = "service-worker" [vars] GITHUB_CLIENT_ID = "" GITHUB_CLIENT_SECRET = "" +SECRET = "" [[kv_namespaces]] binding = "DATAB" From 35c4da3b9a3772fc90a96bafe92da3c2080105be Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Dec 2021 14:49:57 -0500 Subject: [PATCH 02/11] userid -> guestid --- src/models/todolist.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/models/todolist.ts b/src/models/todolist.ts index 8769189..70bc947 100644 --- a/src/models/todolist.ts +++ b/src/models/todolist.ts @@ -19,16 +19,16 @@ export interface Todo { export type TodoList = Todo[]; const TTL = 60 * 60 * 24 * 30; // 30 days, in seconds -export function sync(userid: GuestID, list: TodoList): Promise { - return database.put('todolist', userid, list, { expirationTtl: TTL }); +export function sync(guestid: GuestID, list: TodoList): Promise { + return database.put('todolist', guestid, list, { expirationTtl: TTL }); } -export function lookup(userid: GuestID) { - return database.get('todolist', userid); +export function lookup(guestid: GuestID) { + return database.get('todolist', guestid); } -export async function insert(userid: GuestID, text: string) { - const list = await lookup(userid) || []; +export async function insert(guestid: GuestID, text: string) { + const list = await lookup(guestid) || []; const todo: Todo = { todoid: keys.gen(36), @@ -39,13 +39,13 @@ export async function insert(userid: GuestID, text: string) { list.push(todo); - await sync(userid, list); + await sync(guestid, list); return todo; } -export async function update(userid: GuestID, todoid: TodoID, patch: { text?: string, done?: boolean }) { - const list = await lookup(userid); +export async function update(guestid: GuestID, todoid: TodoID, patch: { text?: string, done?: boolean }) { + const list = await lookup(guestid); if (!list) return; for (const todo of list) { @@ -58,22 +58,22 @@ export async function update(userid: GuestID, todoid: TodoID, patch: { text?: st todo.done = patch.done as boolean; } - await sync(userid, list); + await sync(guestid, list); return todo; } } } -export async function destroy(userid: GuestID, todoid: TodoID) { - const list = await lookup(userid); +export async function destroy(guestid: GuestID, todoid: TodoID) { + const list = await lookup(guestid); let i = list.length; while (i--) { if (list[i].todoid === todoid) { list.splice(i, 1); - await sync(userid, list); + await sync(guestid, list); return; } } From 4b6d3a9637e610ee693b53ff7eb0c47c5e12c4a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Dec 2021 17:46:31 -0500 Subject: [PATCH 03/11] various fixes and tweaks --- src/models/gist.ts | 43 +++++++++++++++++++++--------------------- src/models/session.ts | 2 +- src/models/todolist.ts | 2 +- src/models/user.ts | 2 +- src/routes/gists.ts | 4 ++++ src/routes/sessions.ts | 7 +++---- src/utils/keys.ts | 11 +++++++++-- 7 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/models/gist.ts b/src/models/gist.ts index 498944a..259f712 100644 --- a/src/models/gist.ts +++ b/src/models/gist.ts @@ -13,7 +13,7 @@ export interface File { export type GistID = UID<36>; export interface Gist { - gistid: GistID; + uid: GistID; userid: UserID; name: string; files: File[]; @@ -21,43 +21,44 @@ export interface Gist { updated_at?: TIMESTAMP; } -/** Create new `GistID` value */ -export const toUID = () => keys.gen(36); +export async function deconflict(gistid: string) { + const exists = await database.has('gist', gistid as GistID); + return !exists; +} -/** Find a Gist by its public ID value */ -export async function lookup(gistid: GistID) { +export function lookup(gistid: GistID) { return database.get('gist', gistid); } /** Create a new Gist for the User */ export async function insert(input: Partial, userid: User.UserID): Promise { - const values: Gist = { - // wait until have unique GistID - gistid: await keys.until(toUID, lookup), + const gist: Gist = { + uid: await keys.unique_uid(deconflict), + userid, name: input.name || '', files: input.files || [], - userid, created_at: Date.now() }; - await database.put('gist', values.gistid, values); + await database.put('gist', gist.uid, gist); // synchronize the owner's list of gists - await sync(userid, values); + await sync(userid, gist); // return the new item - return values; + return gist; } /** Update the name and/or files for an existing Gist */ export async function update(values: Gist, changes: Gist): Promise { // carefully choose updated keys - values.name = changes.name || values.name; - values.files = changes.files || values.files; + if ('name' in changes) values.name = changes.name; + if ('files' in changes) values.files = changes.files; + values.updated_at = Date.now(); // update the gist with its new values - await database.put('gist', values.gistid, values); + await database.put('gist', values.uid, values); // synchronize the owner's list of gists await sync(values.userid, values); @@ -69,7 +70,7 @@ export async function update(values: Gist, changes: Gist): Promise { /** Destroy an existing Gist record */ export async function destroy(gist: Gist) { // remove the gist record itself - await database.remove('gist', gist.gistid); + await database.remove('gist', gist.uid); // synchronize the owner's list of gists await sync(gist.userid, gist, true); @@ -79,9 +80,9 @@ export async function destroy(gist: Gist) { export async function sync(userid: UserID, target: Gist | User.UserGist, isRemove = false) { const list = await User.gists(userid); - if (target && target.gistid) { + if (target && target.uid) { for (let i=0; i < list.length; i++) { - if (list[i].gistid === target.gistid) { + if (list[i].uid === target.uid) { list.splice(i, 1); break; } @@ -89,7 +90,7 @@ export async function sync(userid: UserID, target: Gist | User.UserGist, isRemov } isRemove || list.unshift({ - gistid: target.gistid, + uid: target.uid, name: target.name, updated_at: target.updated_at || (target as Gist).created_at, }); @@ -102,6 +103,6 @@ export async function sync(userid: UserID, target: Gist | User.UserGist, isRemov * @NOTE Matches existing `svelte.dev` Gist output */ export function output(gist: Gist) { - const { gistid, name, userid, files } = gist; - return { gistid, owner:userid, name, files }; + const { uid, name, userid, files } = gist; + return { uid, owner: userid, name, files }; } diff --git a/src/models/session.ts b/src/models/session.ts index bb3e177..759bb5a 100644 --- a/src/models/session.ts +++ b/src/models/session.ts @@ -1,7 +1,7 @@ import type { UID } from 'worktop/utils'; import type { UserID } from './user'; -export type SessionID = UID<32>; +export type SessionID = UID<36>; export interface Session { sessionid: SessionID; diff --git a/src/models/todolist.ts b/src/models/todolist.ts index 70bc947..43e3db5 100644 --- a/src/models/todolist.ts +++ b/src/models/todolist.ts @@ -31,7 +31,7 @@ export async function insert(guestid: GuestID, text: string) { const list = await lookup(guestid) || []; const todo: Todo = { - todoid: keys.gen(36), + todoid: keys.uid(36), created_at: Date.now(), text, done: false diff --git a/src/models/user.ts b/src/models/user.ts index 57dad2e..ae6b215 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -20,7 +20,7 @@ export interface User { } // The `Gist` attributes saved for `owner` relationship -export type UserGist = Pick; +export type UserGist = Pick; /** Get all Gists belonging to UserID */ export function gists(userid: UserID): Promise { diff --git a/src/routes/gists.ts b/src/routes/gists.ts index 9b50ec1..67db4d8 100644 --- a/src/routes/gists.ts +++ b/src/routes/gists.ts @@ -20,9 +20,13 @@ export const create = handler(authenticate(async (req, res) => { const userid = req.query.get('userid') as UserID; if (!userid) throw new HttpError('Missing userid', 400); + console.log({ userid }); + const input = await req.body>(); if (!input) throw new HttpError('Missing request body', 400); + console.log({ input }); + // TODO: validate name & files const name = (input.name || '').trim(); const files = ([] as Gist.File[]).concat(input.files || []); diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index 6549625..ed3c052 100644 --- a/src/routes/sessions.ts +++ b/src/routes/sessions.ts @@ -9,9 +9,8 @@ import { authenticate } from '../utils/auth'; // Expires in 1 year (seconds) const EXPIRES = 86400 * 365; -const get_uid = () => keys.gen(32); -const lookup = async (sessionid: SessionID) => { - const exists = await database.has('session', sessionid); +const deconflict = async (sessionid: string) => { + const exists = await database.has('session', sessionid as SessionID); return !exists; } @@ -20,7 +19,7 @@ export const create = handler(authenticate(async (req, res) => { const user = await req.body(); if (!user) throw new HttpError('Missing body', 400); - const sessionid: SessionID = await keys.until(get_uid, lookup); + const sessionid = await keys.unique_uid(deconflict); const expires = Date.now() + EXPIRES * 1000; // create or update the user object diff --git a/src/utils/keys.ts b/src/utils/keys.ts index 4e6dc9d..ad56735 100644 --- a/src/utils/keys.ts +++ b/src/utils/keys.ts @@ -1,8 +1,15 @@ +import { until } from 'worktop/kv'; +import { uid } from 'worktop/utils'; import type { Identifiers } from './database'; export function format(type: K, uid: Identifiers[K]) { return `${type}__${uid}`; } -export { until } from 'worktop/kv'; -export { uid as gen } from 'worktop/utils'; +const get_uid = () => uid(36); + +export function unique_uid(fn: (id: string) => Promise) { + return until(get_uid, fn); +} + +export { uid }; \ No newline at end of file From 4b76fb4e47e70fd4ae7804d76acdcb960fb573b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Dec 2021 17:16:03 -0500 Subject: [PATCH 04/11] remove session/gist stuff --- src/index.ts | 12 ----- src/models/gist.ts | 108 ----------------------------------------- src/models/session.ts | 10 ---- src/models/user.ts | 47 ------------------ src/routes/gists.ts | 77 ----------------------------- src/routes/sessions.ts | 57 ---------------------- src/utils/auth.ts | 14 ------ 7 files changed, 325 deletions(-) delete mode 100644 src/models/gist.ts delete mode 100644 src/models/session.ts delete mode 100644 src/models/user.ts delete mode 100644 src/routes/gists.ts delete mode 100644 src/routes/sessions.ts delete mode 100644 src/utils/auth.ts diff --git a/src/index.ts b/src/index.ts index e9b0fa3..884c939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,6 @@ import { Router } from 'worktop'; import * as CORS from 'worktop/cors'; import * as Cache from 'worktop/cache'; -import * as Gists from './routes/gists'; -import * as Sessions from './routes/sessions'; import * as Todos from './routes/todos'; import * as Docs from './routes/docs'; @@ -12,16 +10,6 @@ API.prepare = CORS.preflight({ maxage: 3600 }); -API.add('POST', '/session', Sessions.create); -API.add('GET', '/session/:sessionid', Sessions.show); -API.add('DELETE', '/session/:sessionid', Sessions.destroy); - -API.add('GET', '/gists', Gists.list); -API.add('POST', '/gists', Gists.create); -API.add('GET', '/gists/:gistid', Gists.show); -API.add('PUT', '/gists/:gistid', Gists.update); -API.add('DELETE', '/gists/:gistid', Gists.destroy); - API.add('GET', '/todos/:guestid', Todos.list); API.add('POST', '/todos/:guestid', Todos.create); API.add('PATCH', '/todos/:guestid/:todoid', Todos.update); diff --git a/src/models/gist.ts b/src/models/gist.ts deleted file mode 100644 index 259f712..0000000 --- a/src/models/gist.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as User from './user'; -import * as database from '../utils/database'; -import * as keys from '../utils/keys'; - -import type { UserID } from './user'; -import type { UID } from 'worktop/utils'; - -export interface File { - name: string; - source: string; -} - -export type GistID = UID<36>; - -export interface Gist { - uid: GistID; - userid: UserID; - name: string; - files: File[]; - created_at: TIMESTAMP; - updated_at?: TIMESTAMP; -} - -export async function deconflict(gistid: string) { - const exists = await database.has('gist', gistid as GistID); - return !exists; -} - -export function lookup(gistid: GistID) { - return database.get('gist', gistid); -} - -/** Create a new Gist for the User */ -export async function insert(input: Partial, userid: User.UserID): Promise { - const gist: Gist = { - uid: await keys.unique_uid(deconflict), - userid, - name: input.name || '', - files: input.files || [], - created_at: Date.now() - }; - - await database.put('gist', gist.uid, gist); - - // synchronize the owner's list of gists - await sync(userid, gist); - - // return the new item - return gist; -} - -/** Update the name and/or files for an existing Gist */ -export async function update(values: Gist, changes: Gist): Promise { - // carefully choose updated keys - if ('name' in changes) values.name = changes.name; - if ('files' in changes) values.files = changes.files; - - values.updated_at = Date.now(); - - // update the gist with its new values - await database.put('gist', values.uid, values); - - // synchronize the owner's list of gists - await sync(values.userid, values); - - // return the updated item - return values; -} - -/** Destroy an existing Gist record */ -export async function destroy(gist: Gist) { - // remove the gist record itself - await database.remove('gist', gist.uid); - - // synchronize the owner's list of gists - await sync(gist.userid, gist, true); -} - -/** Synchronize UserGists list for User/Owner */ -export async function sync(userid: UserID, target: Gist | User.UserGist, isRemove = false) { - const list = await User.gists(userid); - - if (target && target.uid) { - for (let i=0; i < list.length; i++) { - if (list[i].uid === target.uid) { - list.splice(i, 1); - break; - } - } - } - - isRemove || list.unshift({ - uid: target.uid, - name: target.name, - updated_at: target.updated_at || (target as Gist).created_at, - }); - - return database.put('owner', userid, list); -} - -/** - * Format a Gist for API response - * @NOTE Matches existing `svelte.dev` Gist output - */ -export function output(gist: Gist) { - const { uid, name, userid, files } = gist; - return { uid, owner: userid, name, files }; -} diff --git a/src/models/session.ts b/src/models/session.ts deleted file mode 100644 index 759bb5a..0000000 --- a/src/models/session.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { UID } from 'worktop/utils'; -import type { UserID } from './user'; - -export type SessionID = UID<36>; - -export interface Session { - sessionid: SessionID; - userid: UserID; - expires: TIMESTAMP; -} \ No newline at end of file diff --git a/src/models/user.ts b/src/models/user.ts deleted file mode 100644 index ae6b215..0000000 --- a/src/models/user.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as database from '../utils/database'; - -import type { Gist } from './gist'; - -export type UserID = string; - -export interface User { - /** GitHub ID */ - id: UserID; - /** GitHub username */ - username: string; - /** First + Last */ - name: string; - /** GitHub avatar */ - avatar: string; - /** GitHub oAuth token */ - token: GitHub.AccessToken; - created_at?: TIMESTAMP; - updated_at?: TIMESTAMP; -} - -// The `Gist` attributes saved for `owner` relationship -export type UserGist = Pick; - -/** Get all Gists belonging to UserID */ -export function gists(userid: UserID): Promise { - return database.get('owner', userid).then(arr => arr || []); -} - -export async function upsert(user: Omit, 'updated_at'>) { - let record; - - try { - record = await database.get('user', user.id); - } catch { - record = {}; - } - - const now = Date.now(); - - await database.put('user', user.id, { - created_at: now, - ...record, - ...user, - updated_at: now - }); -} \ No newline at end of file diff --git a/src/routes/gists.ts b/src/routes/gists.ts deleted file mode 100644 index 67db4d8..0000000 --- a/src/routes/gists.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as Gist from '../models/gist'; -import * as User from '../models/user'; -import { HttpError } from '../utils/error'; - -import type { GistID } from '../models/gist'; -import type { UserID } from '../models/user'; -import { handler } from '../utils/handler'; -import { authenticate } from '../utils/auth'; - -// GET /gists?userid=userid -export const list = handler(authenticate(async (req, res) => { - const userid = req.query.get('userid') as UserID; - if (!userid) throw new HttpError('Missing userid', 400); - - res.send(200, await User.gists(userid)); -})); - -// POST /gists?userid=userid -export const create = handler(authenticate(async (req, res) => { - const userid = req.query.get('userid') as UserID; - if (!userid) throw new HttpError('Missing userid', 400); - - console.log({ userid }); - - const input = await req.body>(); - if (!input) throw new HttpError('Missing request body', 400); - - console.log({ input }); - - // TODO: validate name & files - const name = (input.name || '').trim(); - const files = ([] as Gist.File[]).concat(input.files || []); - - const item = await Gist.insert({ name, files }, userid); - res.send(201, Gist.output(item)); -})); - -// GET /gists/:gistid -export const show = handler(async (req, res) => { - const item = await Gist.lookup(req.params.gistid as GistID); - res.send(200, Gist.output(item)); -}); - -// PUT /gists/:gistid -export const update = handler(authenticate(async (req, res) => { - const userid = req.query.get('userid') as UserID; - if (!userid) throw new HttpError('Missing userid', 400); - - const item = await Gist.lookup(req.params.gistid as GistID); - - if (userid !== item.userid) { - throw new HttpError('Gist does not belong to you', 403); - } - - const input = await req.body(); - if (!input) throw new HttpError('Missing request body', 400); - - const values = await Gist.update(item, input); - - res.send(200, Gist.output(values)); -})); - -// DELETE /gists/:gistid -export const destroy = handler(authenticate(async (req, res) => { - const userid = req.query.get('userid') as UserID; - if (!userid) throw new HttpError('Missing userid', 400); - - const item = await Gist.lookup(req.params.gistid as GistID); - - if (userid !== item.userid) { - throw new HttpError('Gist does not belong to you', 403); - } - - await Gist.destroy(item); - - res.send(204); -})); diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts deleted file mode 100644 index ed3c052..0000000 --- a/src/routes/sessions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { handler } from '../utils/handler'; -import * as database from '../utils/database'; -import * as keys from '../utils/keys'; -import { SessionID } from '../models/session'; -import * as User from '../models/user'; -import { HttpError } from '../utils/error'; -import { authenticate } from '../utils/auth'; - -// Expires in 1 year (seconds) -const EXPIRES = 86400 * 365; - -const deconflict = async (sessionid: string) => { - const exists = await database.has('session', sessionid as SessionID); - return !exists; -} - -// POST /session -export const create = handler(authenticate(async (req, res) => { - const user = await req.body(); - if (!user) throw new HttpError('Missing body', 400); - - const sessionid = await keys.unique_uid(deconflict); - const expires = Date.now() + EXPIRES * 1000; - - // create or update the user object - await User.upsert(user); - - // create a session - await database.put('session', sessionid, { - sessionid, - userid: user.id, - expires - }); - - res.send(200, { sessionid, expires }); -})); - -// GET /session/:sessionid -export const show = handler(authenticate(async (req, res) => { - const { userid } = await database.get('session', req.params.sessionid as SessionID); - const user = await database.get('user', userid); - - res.send(200, { - user: { - id: user.id, - name: user.name, - username: user.username, - avatar: user.avatar - } - }); -})); - -// DELETE /session/:sessionid -export const destroy = handler(authenticate(async (req, res) => { - await database.remove('session', req.params.sessionid as SessionID); - res.send(204); -})); \ No newline at end of file diff --git a/src/utils/auth.ts b/src/utils/auth.ts deleted file mode 100644 index e5f1ec3..0000000 --- a/src/utils/auth.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Handler } from 'worktop'; -import { HttpError } from './error'; - -declare var SECRET: string; - -export function authenticate(handler: Handler): Handler { - return function (req, res) { - if (req.headers.get('authorization') !== `Basic ${SECRET}`) { - throw new HttpError('Unauthorized', 401); - } - - return handler(req, res); - }; -} \ No newline at end of file From e1e537328b25c6fc55f08a5dd9eef519c8bcab3e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Dec 2021 17:21:26 -0500 Subject: [PATCH 05/11] linting --- src/utils/database.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/utils/database.ts b/src/utils/database.ts index 88331e0..f99f3bd 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -1,28 +1,17 @@ import * as keys from './keys'; +import { HttpError } from './error'; import type { KV } from 'worktop/kv'; -import type { Gist, GistID } from '../models/gist'; -import type { Session, SessionID } from '../models/session'; import type { TodoList, GuestID } from '../models/todolist'; -import type { User, UserGist, UserID } from '../models/user'; -import { HttpError } from './error'; declare const DATAB: KV.Namespace; export interface Identifiers { - gist: GistID; - owner: UserID; - session: SessionID; todolist: GuestID; - user: UserID; } export interface Models { - gist: Gist; - owner: UserGist[]; - session: Session; todolist: TodoList; - user: User; } export function has(type: K, uid: Identifiers[K]): Promise { From 6c328591b6df96883a35247e748c80bbc22d1f6a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Dec 2021 17:22:09 -0500 Subject: [PATCH 06/11] remove unused has method --- src/utils/database.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/database.ts b/src/utils/database.ts index f99f3bd..428c170 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -14,11 +14,6 @@ export interface Models { todolist: TodoList; } -export function has(type: K, uid: Identifiers[K]): Promise { - const keyname = keys.format(type, uid); - return DATAB.get(keyname).then(() => true, () => false); -} - export function get(type: K, uid: Identifiers[K]): Promise { const keyname = keys.format(type, uid); From 121132e2562db1ac450fc63a29550f239e48510a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Dec 2021 18:45:32 -0500 Subject: [PATCH 07/11] remove SECRET and github stuff --- .github/workflows/tag.yml | 2 -- cfw.js | 2 -- wrangler.example.toml | 3 --- 3 files changed, 7 deletions(-) diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index a306a2d..260e058 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -15,8 +15,6 @@ env: # worker globals/secrets CLOUDFLARE_NAMESPACEID: ${{ secrets.CLOUDFLARE_NAMESPACEID }} CLOUDFLARE_NAMESPACEID_DOCS: ${{ secrets.CLOUDFLARE_NAMESPACEID_DOCS }} - GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }} - GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} jobs: deploy: diff --git a/cfw.js b/cfw.js index cb835dc..1405b4d 100644 --- a/cfw.js +++ b/cfw.js @@ -13,7 +13,5 @@ module.exports = { globals: { DATAB: `KV:${VARS.CLOUDFLARE_NAMESPACEID}`, DOCS: `KV:${VARS.CLOUDFLARE_NAMESPACEID_DOCS}`, - GITHUB_CLIENT_ID: `ENV:${VARS.GITHUB_CLIENT_ID}`, - GITHUB_CLIENT_SECRET: `SECRET:${VARS.GITHUB_CLIENT_SECRET}`, } } diff --git a/wrangler.example.toml b/wrangler.example.toml index 45bda8a..38d708f 100644 --- a/wrangler.example.toml +++ b/wrangler.example.toml @@ -25,9 +25,6 @@ command = "npm run build" upload.format = "service-worker" [vars] -GITHUB_CLIENT_ID = "" -GITHUB_CLIENT_SECRET = "" -SECRET = "" [[kv_namespaces]] binding = "DATAB" From f9d7ee6b373b0bc7eeb2d87465de2688affb1dd9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Dec 2021 18:45:56 -0500 Subject: [PATCH 08/11] uncomment --- src/utils/handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/handler.ts b/src/utils/handler.ts index 93ae3ec..956a8ae 100644 --- a/src/utils/handler.ts +++ b/src/utils/handler.ts @@ -9,9 +9,9 @@ export function handler(fn: Handler): Handler { const status = (err as HttpError).statusCode || 500; const message = (err as HttpError).message; - // if (status >= 500) { + if (status >= 500) { console.error((err as HttpError).stack); - // } + } res.send(status, { status, message }); } From bbfa0573035fe0d02d957d5a0682f28cbb22101c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Dec 2021 18:48:55 -0500 Subject: [PATCH 09/11] remove some unused code --- src/utils/keys.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/utils/keys.ts b/src/utils/keys.ts index ad56735..70f9153 100644 --- a/src/utils/keys.ts +++ b/src/utils/keys.ts @@ -1,4 +1,3 @@ -import { until } from 'worktop/kv'; import { uid } from 'worktop/utils'; import type { Identifiers } from './database'; @@ -6,10 +5,4 @@ export function format(type: K, uid: Identifiers[K] return `${type}__${uid}`; } -const get_uid = () => uid(36); - -export function unique_uid(fn: (id: string) => Promise) { - return until(get_uid, fn); -} - export { uid }; \ No newline at end of file From 670f8ff6709c36d757a67b49db92955c14a38f8d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Dec 2021 18:50:16 -0500 Subject: [PATCH 10/11] revert change, for easier diff --- src/index.ts | 8 ++++---- src/models/todolist.ts | 26 +++++++++++++------------- src/routes/todos.ts | 22 +++++++++++----------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index 884c939..2d81478 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,10 +10,10 @@ API.prepare = CORS.preflight({ maxage: 3600 }); -API.add('GET', '/todos/:guestid', Todos.list); -API.add('POST', '/todos/:guestid', Todos.create); -API.add('PATCH', '/todos/:guestid/:todoid', Todos.update); -API.add('DELETE', '/todos/:guestid/:todoid', Todos.destroy); +API.add('GET', '/todos/:userid', Todos.list); +API.add('POST', '/todos/:userid', Todos.create); +API.add('PATCH', '/todos/:userid/:todoid', Todos.update); +API.add('DELETE', '/todos/:userid/:todoid', Todos.destroy); API.add('GET', '/docs/:project/:type', Docs.list); API.add('GET', '/docs/:project/:type/:slug', Docs.entry); diff --git a/src/models/todolist.ts b/src/models/todolist.ts index 43e3db5..50a294b 100644 --- a/src/models/todolist.ts +++ b/src/models/todolist.ts @@ -19,16 +19,16 @@ export interface Todo { export type TodoList = Todo[]; const TTL = 60 * 60 * 24 * 30; // 30 days, in seconds -export function sync(guestid: GuestID, list: TodoList): Promise { - return database.put('todolist', guestid, list, { expirationTtl: TTL }); +export function sync(userid: GuestID, list: TodoList): Promise { + return database.put('todolist', userid, list, { expirationTtl: TTL }); } -export function lookup(guestid: GuestID) { - return database.get('todolist', guestid); +export function lookup(userid: GuestID) { + return database.get('todolist', userid); } -export async function insert(guestid: GuestID, text: string) { - const list = await lookup(guestid) || []; +export async function insert(userid: GuestID, text: string) { + const list = await lookup(userid) || []; const todo: Todo = { todoid: keys.uid(36), @@ -39,13 +39,13 @@ export async function insert(guestid: GuestID, text: string) { list.push(todo); - await sync(guestid, list); + await sync(userid, list); return todo; } -export async function update(guestid: GuestID, todoid: TodoID, patch: { text?: string, done?: boolean }) { - const list = await lookup(guestid); +export async function update(userid: GuestID, todoid: TodoID, patch: { text?: string, done?: boolean }) { + const list = await lookup(userid); if (!list) return; for (const todo of list) { @@ -58,22 +58,22 @@ export async function update(guestid: GuestID, todoid: TodoID, patch: { text?: s todo.done = patch.done as boolean; } - await sync(guestid, list); + await sync(userid, list); return todo; } } } -export async function destroy(guestid: GuestID, todoid: TodoID) { - const list = await lookup(guestid); +export async function destroy(userid: GuestID, todoid: TodoID) { + const list = await lookup(userid); let i = list.length; while (i--) { if (list[i].todoid === todoid) { list.splice(i, 1); - await sync(guestid, list); + await sync(userid, list); return; } } diff --git a/src/routes/todos.ts b/src/routes/todos.ts index 65c94bb..be93a14 100644 --- a/src/routes/todos.ts +++ b/src/routes/todos.ts @@ -6,40 +6,40 @@ import type { Params } from 'worktop/request'; import type { TodoID, GuestID } from '../models/todolist'; import { handler } from '../utils/handler'; -type ParamsUserID = Params & { guestid: GuestID }; +type ParamsUserID = Params & { userid: GuestID }; -// GET /todos/:guestid +// GET /todos/:userid export const list: Handler = handler(async (req, res) => { - const todos = await TodoList.lookup(req.params.guestid); + const todos = await TodoList.lookup(req.params.userid); res.send(200, todos); }); -// POST /todos/:guestid +// POST /todos/:userid export const create: Handler = handler(async (req, res) => { const input = await req.body<{ text: string }>(); if (!input) throw new HttpError('Missing request body', 400); - const todo = await TodoList.insert(req.params.guestid, input.text); + const todo = await TodoList.insert(req.params.userid, input.text); res.send(201, todo); }); -// PATCH /todos/:guestid/:todoid +// PATCH /todos/:userid/:todoid export const update: Handler = handler(async (req, res) => { - const { guestid, todoid } = req.params; + const { userid, todoid } = req.params; const input = await req.body<{ text?: string, done?: boolean }>(); if (!input) throw new HttpError('Missing request body', 400); - const todo = await TodoList.update(guestid, todoid as TodoID, input); + const todo = await TodoList.update(userid, todoid as TodoID, input); res.send(200, todo); }); -// DELETE /todos/:guestid/:todoid +// DELETE /todos/:userid/:todoid export const destroy: Handler = handler(async (req, res) => { - const { guestid, todoid } = req.params; + const { userid, todoid } = req.params; - await TodoList.destroy(guestid, todoid as TodoID); + await TodoList.destroy(userid, todoid as TodoID); res.send(200, {}); // TODO should be a 204, no? }); From cb6dfd2942928e6402f9a716b89802771b01c3ec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Dec 2021 18:51:07 -0500 Subject: [PATCH 11/11] revert more stuff --- src/index.ts | 4 ++-- src/models/todolist.ts | 12 ++++++------ src/routes/todos.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2d81478..f4e90b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,8 @@ API.prepare = CORS.preflight({ API.add('GET', '/todos/:userid', Todos.list); API.add('POST', '/todos/:userid', Todos.create); -API.add('PATCH', '/todos/:userid/:todoid', Todos.update); -API.add('DELETE', '/todos/:userid/:todoid', Todos.destroy); +API.add('PATCH', '/todos/:userid/:uid', Todos.update); +API.add('DELETE', '/todos/:userid/:uid', Todos.destroy); API.add('GET', '/docs/:project/:type', Docs.list); API.add('GET', '/docs/:project/:type/:slug', Docs.entry); diff --git a/src/models/todolist.ts b/src/models/todolist.ts index 50a294b..997ba23 100644 --- a/src/models/todolist.ts +++ b/src/models/todolist.ts @@ -10,7 +10,7 @@ export type TodoID = UID<36>; export type GuestID = string; export interface Todo { - todoid: TodoID; + uid: TodoID; created_at: TIMESTAMP; text: string; done: boolean; @@ -31,7 +31,7 @@ export async function insert(userid: GuestID, text: string) { const list = await lookup(userid) || []; const todo: Todo = { - todoid: keys.uid(36), + uid: keys.uid(36), created_at: Date.now(), text, done: false @@ -44,12 +44,12 @@ export async function insert(userid: GuestID, text: string) { return todo; } -export async function update(userid: GuestID, todoid: TodoID, patch: { text?: string, done?: boolean }) { +export async function update(userid: GuestID, uid: TodoID, patch: { text?: string, done?: boolean }) { const list = await lookup(userid); if (!list) return; for (const todo of list) { - if (todo.todoid === todoid) { + if (todo.uid === uid) { if ('text' in patch) { todo.text = patch.text as string; } @@ -65,12 +65,12 @@ export async function update(userid: GuestID, todoid: TodoID, patch: { text?: st } } -export async function destroy(userid: GuestID, todoid: TodoID) { +export async function destroy(userid: GuestID, uid: TodoID) { const list = await lookup(userid); let i = list.length; while (i--) { - if (list[i].todoid === todoid) { + if (list[i].uid === uid) { list.splice(i, 1); await sync(userid, list); diff --git a/src/routes/todos.ts b/src/routes/todos.ts index be93a14..38f2445 100644 --- a/src/routes/todos.ts +++ b/src/routes/todos.ts @@ -24,22 +24,22 @@ export const create: Handler = handler(async (req, res) => { res.send(201, todo); }); -// PATCH /todos/:userid/:todoid +// PATCH /todos/:userid/:uid export const update: Handler = handler(async (req, res) => { - const { userid, todoid } = req.params; + const { userid, uid } = req.params; const input = await req.body<{ text?: string, done?: boolean }>(); if (!input) throw new HttpError('Missing request body', 400); - const todo = await TodoList.update(userid, todoid as TodoID, input); + const todo = await TodoList.update(userid, uid as TodoID, input); res.send(200, todo); }); -// DELETE /todos/:userid/:todoid +// DELETE /todos/:userid/:uid export const destroy: Handler = handler(async (req, res) => { - const { userid, todoid } = req.params; + const { userid, uid } = req.params; - await TodoList.destroy(userid, todoid as TodoID); + await TodoList.destroy(userid, uid as TodoID); res.send(200, {}); // TODO should be a 204, no? });