X-Git-Url: https://git.xangelo.ca/?p=rss-reader.git;a=blobdiff_plain;f=src%2Fserver.ts;h=7add8509c2cbeb6b322eef587862f09fe159d502;hp=4e1404e5385254aff7098fa56191ee991e2f3cf7;hb=200808f1fc03c8669968a77074bc6051d3bf663b;hpb=1efc77a5994e43c58f326016acfe9e48da055b56 diff --git a/src/server.ts b/src/server.ts index 4e1404e..7add850 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,23 +6,44 @@ import { ingest, ingestSingle } from './ingester'; import {RSSParser} from './parsers/rss'; import bodyParser from 'body-parser'; import {BaseParser} from './parsers/base'; +import fs from 'fs'; +import { promisify } from 'util'; - +const HTML_ROOT = join(__dirname, '..', 'html'); const app = express(); +const session: Map = new Map(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true})); -app.use(express.static(join(__dirname, '..', 'html'))); app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); type WrappedApiHandler = (req: Request, res: Response) => Promise; +type WrapOptions = { + auth?: boolean; +} -function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: WrappedApiHandler, view?: (args: any) => string) : void { +function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (args: any) => string) : void { app[method](endpoint, async(req, res) => { try { + if(options.auth) { + if(!req.headers['hx-current-url']) { + throw new Error('Invald user'); + } + const query = new URLSearchParams(req.headers['hx-current-url'].toString().split('?')[1]); + const token = query.get('token'); + const id = query.get('id'); + + if(!token || !id) { + throw new Error('Invalid user'); + } + if(!session.get(token) || session.get(token) !== id) { + throw new Error('Invalid user'); + } + } + const output = await fn(req, res); if(req.headers['content-type'] === 'application/json') { res.json(output); @@ -37,7 +58,12 @@ function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: Wra } } - res.status(204); + if(!output) { + res.status(204); + return; + } + + return res.json(output); } catch(e) { console.error(e); @@ -49,23 +75,97 @@ function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: Wra }); } -function apiGet(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void { - apiWrapper('get', endpoint, fn, view); +app.get('/', (req, res) => { + res.sendFile(join(HTML_ROOT, 'index.html')); +}); +app.get('/home.css', (req, res) => { + res.sendFile(join(HTML_ROOT, 'home.css')); +}); +app.get('/style.css', (req, res) => { + res.sendFile(join(HTML_ROOT, 'style.css')); +}); + +function apiGet(endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void { + apiWrapper('get', endpoint, options, fn, view); } -function apiPost(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void { - apiWrapper('post', endpoint, fn, view); +function apiPost(endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void { + apiWrapper('post', endpoint, options, fn, view); } -function apiDelete(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void { - apiWrapper('delete', endpoint, fn, view); +function apiDelete(endpoint: string, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void { + apiWrapper('delete', endpoint, options, fn, view); } -apiPost('/login', async (req, res): Promise => { +apiPost('/login', {auth: false}, async (req, res): Promise => { + const email = req.body.email; + const loginCode = uuidv4().substr(0, 6).toUpperCase(); + + const account = query.createLoginCode(email, loginCode); + const token = uuidv4(); + + const login_link = `/app?code=${account.login_code}&id=${account.id}` + console.log('login link:', login_link); + + // this should actually just email the link and return some text + // about what a great person you are. + return { + login: login_link + } }); -apiPost('/feeds', async (req, res): Promise => { +apiGet('/app', {auth: false}, async (req, res) => { + const id = req.query.id?.toString(); + const token = req.query.token?.toString(); + const code = req.query.code?.toString(); + + + if(code && id) { + console.log('validating', id, code); + if(!query.validateLoginCode(id, code)) { + throw new Error('Invalid login'); + } + let token = uuidv4(); + let i = 0; + while(session.has(token) && i < 10) { + token = uuidv4(); + ++i; + } + + if(i >= 10) { + throw new Error('Please login again'); + } + + session.set(token, id); + res.redirect(`/app?id=${id}&token=${token}`); + return; + } + + if(token && id) { + // validate it. + if(!session.has(token) || session.get(token) !== id) { + res.redirect('/'); + return; + } + const data = await promisify(fs.readFile)(join(HTML_ROOT, 'app.html'), 'utf-8'); + + return { + html: data, + account_id: id, + token: token + }; + } + + res.redirect('/'); + return; + +}, data => { + return data.html.replace(/{ACCOUNT_ID}/g, data.account_id); +}); + +apiPost('/accounts/:account_id/feeds', {auth: true}, async (req, res): Promise => { // get info about the feed const url = req.body.link; + const account_id = req.params.account_id; let parser: BaseParser; @@ -75,27 +175,27 @@ apiPost('/feeds', async (req, res): Promise => { const feedData = await parser.parse(url); const title = feedData.title; - const id = uuidv4(); // ingest teh feed, - query.addFeed.run(id, title, url); + const feed = query.addFeed(title, url, account_id); - await ingestSingle(id); + await ingestSingle(feed.id); res.setHeader('HX-Trigger', 'newFeed'); return { - id, + id: feed.id, title, url } }); -apiGet('/feeds', async (req, res): Promise => { - const feeds = query.getFeedList.all(); +apiGet('/accounts/:account_id/feeds', {auth: true}, async (req, res): Promise => { + const account_id = req.params.account_id; + const feeds = query.getFeedList(account_id); // get unread counts - const unread_count = query.getUnreadCountForAll.all(); + const unread_count = query.getUnreadCountForAll(account_id); const feedsWithUnread = feeds.map(feed => { const unread = unread_count.filter(i => i.feed_id === feed.id); @@ -108,7 +208,8 @@ apiGet('/feeds', async (req, res): Promise => { }); return { - feeds: feedsWithUnread + feeds: feedsWithUnread, + account_id: account_id } }, (output: any): string => { const feeds = output.feeds; @@ -116,7 +217,7 @@ apiGet('/feeds', async (req, res): Promise => { const display = feed.unread ? `(${feed.unread})` : ''; const first = idx === 0; - return `
  • ${feed.title} + return `
  • ${feed.title} ${display}
  • ` }).join("\n")}`; @@ -128,7 +229,7 @@ function reasonable(date: Date): string { return `${date.getFullYear()}-${month}-${day}`; } -apiGet('/feeds/:feed_id', async (req, res): Promise => { +apiGet('/accounts/:account_id/feeds/:feed_id',{auth: true}, async (req, res): Promise => { const id = req.params.feed_id; return query.getFeedInfo.get(id); }, (feed: any): string => { @@ -138,18 +239,20 @@ apiGet('/feeds/:feed_id', async (req, res): Promise => { `; }); -apiGet('/feeds/:feed_id/items', async (req, res): Promise => { - const id = req.params.feed_id; +apiGet('/accounts/:account_id/feeds/:feed_id/items',{auth: true}, async (req, res): Promise => { + const { account_id, feed_id } = req.params; + return { - items: query.getFeedsFor.all(id, 0), - info: query.getFeedInfo.get(id) + items: query.getFeedsFor.all(feed_id, 0), + info: query.getFeedInfo.get(feed_id), + account_id } }, (feedData: any): string => { return `
    Feed: ${feedData.info.title}
    @@ -161,7 +264,7 @@ apiGet('/feeds/:feed_id/items', async (req, res): Promise => { const read = !!item.read_at; const first = index === 0; return `
  • - ${item.title} + ${item.title} ${reasonable(new Date(item.pub_date * 1000))}
  • @@ -170,15 +273,21 @@ apiGet('/feeds/:feed_id/items', async (req, res): Promise => { `; }); -apiPost('/feeds/:feed_id/items/markAsRead', async (req, res): Promise => { - const id = req.params.feed_id; - query.readAllItems.run(id); +apiPost('/accounts/:account_id/feeds/:feed_id/items/markAsRead',{auth: true}, async (req, res): Promise => { + const {account_id, feed_id} = req.params; + if(!query.isFeedOwnedBy(account_id, feed_id)) { + throw new Error('Invalid feed'); + } + query.readAllItems(feed_id); return; }); -apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => { - const feed_id = req.params.feed_id; - const item_id = req.params.item_id; +apiGet('/accounts/:account_id/feeds/:feed_id/items/:item_id',{auth: true}, async (req, res) => { + const {account_id, feed_id, item_id} = req.params; + + if(!query.isFeedOwnedBy(account_id, feed_id)) { + throw new Error('Invalid feed'); + } query.readItem.run(Date.now(), item_id); return query.getFeedItemInfo.get(item_id, feed_id); @@ -192,11 +301,10 @@ apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => { Read: ${new Date(output.read_at || undefined)}
    ${output.content}
    - ` - return output.content; + `; }); -apiDelete('/feeds/:feed_id', async (req, res) => { +apiDelete('/feeds/:feed_id',{auth: true}, async (req, res) => { const id = req.params.feed_id; query.deleteFeed.run(id);