diff --git a/package-lock.json b/package-lock.json index c84f5146a..a58415e5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -535,6 +535,11 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", diff --git a/package.json b/package.json index 45abeeda3..8adaf5b63 100644 --- a/package.json +++ b/package.json @@ -99,5 +99,7 @@ "sourceMap": true, "instrument": true }, - "dependencies": {} + "dependencies": { + "cookie": "^0.4.0" + } } diff --git a/site/migrations/002-create-sessions.js b/site/migrations/002-create-sessions.js new file mode 100644 index 000000000..c24fc6911 --- /dev/null +++ b/site/migrations/002-create-sessions.js @@ -0,0 +1,15 @@ +exports.up = DB => { + DB.sql(` + create table if not exists sessions ( + uid uuid NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + user_id integer REFERENCES users(id) not null, + expiry timestamp without time zone DEFAULT now() + interval '1 year' + ); + `); +}; + +exports.down = DB => { + DB.sql(` + drop table if exists sessions; + `); +}; diff --git a/site/package-lock.json b/site/package-lock.json index 373c0505c..f80a81e9d 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -3840,6 +3840,11 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/site/package.json b/site/package.json index 0b949f5da..3b98cbb85 100644 --- a/site/package.json +++ b/site/package.json @@ -24,6 +24,7 @@ "polka": "^1.0.0-next.4", "prismjs": "^1.17.1", "sirv": "^0.4.2", + "uuid": "^3.3.2", "yootils": "0.0.16" }, "devDependencies": { diff --git a/site/src/backend/auth.js b/site/src/backend/auth.js deleted file mode 100644 index 7792f4ee3..000000000 --- a/site/src/backend/auth.js +++ /dev/null @@ -1,150 +0,0 @@ -import polka from 'polka'; -import devalue from 'devalue'; -import send from '@polka/send'; -import { get, post } from 'httpie'; -import { parse, stringify } from 'querystring'; -import { decode, sign, verify } from './token'; -import { find, query } from '../utils/db'; - -const { - BASEURL, - GITHUB_CLIENT_ID, - GITHUB_CLIENT_SECRET, -} = process.env; - -const OAuth = 'https://github.com/login/oauth'; - -function exit(res, code, msg='') { - const error = msg.charAt(0).toUpperCase() + msg.substring(1); - send(res, code, { error }); -} - -function onError(err, req, res) { - const error = err.message || err; - const code = err.code || err.status || 500; - res.headersSent || send(res, code, { error }); -} - -/** - * Middleware to determine User validity - */ -export async function isUser(req, res) { - const abort = exit.bind(null, res, 401); - - const auth = req.headers.authorization; - if (!auth) return abort('Missing Authorization header'); - - const [scheme, token] = auth.split(' '); - if (scheme !== 'Bearer' || !token) return abort('Invalid Authorization format'); - - let data; - const decoded = decode(token, { complete:true }); - if (!decoded || !decoded.header) return abort('Invalid token'); - - try { - data = await verify(token); - } catch (err) { - return abort(err.message); - } - - const { uid, username } = data; - if (!uid || !username) return abort('Invalid token payload'); - - try { - const row = await find(`select * from users where uid = $1 and username = $2 limit 1`, [uid, username]); - return row || abort('Invalid token'); - } catch (err) { - console.error('Auth.isUser', err); - return send(res, 500, 'Unknown error occurred'); - } -} - -export function toUser(obj={}) { - const { uid, username, name, avatar } = obj; - const token = sign({ uid, username }); - return { uid, username, name, avatar, token }; -} - -export function API() { - const app = polka({ onError }); - - if (GITHUB_CLIENT_ID) { - app.get('/auth/login', (req, res) => { - try { - const Location = `${OAuth}/authorize?` + stringify({ - scope: 'read:user', - client_id: GITHUB_CLIENT_ID, - redirect_uri: `${BASEURL}/auth/callback`, - }); - - send(res, 302, Location, { Location }); - } catch (err) { - console.error('GET /auth/login', err); - send(res, 500); - } - }); - - app.get('/auth/callback', async (req, res) => { - try { - // Trade "code" for "access_token" - const r1 = await post(`${OAuth}/access_token?` + stringify({ - code: req.query.code, - client_id: GITHUB_CLIENT_ID, - client_secret: GITHUB_CLIENT_SECRET, - })); - - // Now fetch User details - const { access_token } = parse(r1.data); - const r2 = await get('https://api.github.com/user', { - headers: { - 'User-Agent': 'svelte.dev', - Authorization: `token ${access_token}` - } - }); - - const { id, name, avatar_url, login } = r2.data; - - // Upsert `users` table - const [user] = await query(` - insert into users(uid, name, username, avatar, github_token) - values ($1, $2, $3, $4, $5) on conflict (uid) do update - set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now()) - returning * - `, [id, name, login, avatar_url, access_token]); - - send(res, 200, ` - - `, { - 'Content-Type': 'text/html; charset=utf-8' - }); - } catch (err) { - console.error('GET /auth/callback', err); - send(res, 500, err.data, { - 'Content-Type': err.headers['content-type'], - 'Content-Length': err.headers['content-length'] - }); - } - }); - } else { - // Print "Misconfigured" error - app.get('/auth/login', (req, res) => { - send(res, 500, ` - -

Missing .env file

-

In order to use GitHub authentication, you will need to register an OAuth application and create a local .env file:

-
GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000
-

The BASEURL variable should match the callback URL specified for your app.

-

See also here

- - `, { - 'Content-Type': 'text/html; charset=utf-8' - }); - }); - } - - return app; -} diff --git a/site/src/backend/token.js b/site/src/backend/token.js deleted file mode 100644 index 9adf7d24f..000000000 --- a/site/src/backend/token.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as jwt from 'jsonwebtoken'; - -const { JWT_KEY, JWT_ALG, JWT_EXP } = process.env; - -const CONFIG = { - expiresIn: JWT_EXP, - issuer: 'https://svelte.dev', - audience: 'https://svelte.dev', - algorithm: JWT_ALG, -}; - -export const decode = jwt.decode; - -export function sign(obj, opts, cb) { - opts = Object.assign({}, opts, CONFIG); - return jwt.sign(obj, JWT_KEY, opts, cb); -} - -export function verify(str, opts, cb) { - opts = Object.assign({}, opts, CONFIG); - return jwt.verify(str, JWT_KEY, opts, cb); -} diff --git a/site/src/routes/auth/me.json.js b/site/src/routes/auth/me.json.js deleted file mode 100644 index f77eaf5b9..000000000 --- a/site/src/routes/auth/me.json.js +++ /dev/null @@ -1,8 +0,0 @@ -import send from '@polka/send'; -import { isUser, toUser } from '../../backend/auth'; - -export async function get(req, res) { - const user = await isUser(req, res); - res.setHeader('Cache-Control', 'private, no-cache, no-store'); - return send(res, 200, user ? toUser(user) : null); -} diff --git a/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte b/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte index 1404c1d5f..9b54e80f9 100644 --- a/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte +++ b/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte @@ -1,15 +1,24 @@
{name} - {name} avatar + {name} avatar {#if showMenu}