X-Git-Url: https://git.xangelo.ca/?p=rss-reader.git;a=blobdiff_plain;f=src%2Fserver.ts;h=e9915d3947216e6188c5ba58ff4c6c84a85c919b;hp=6c9453275605ae8ab687f9b2f0ae57b86b66b973;hb=HEAD;hpb=6e5224bcdf1fcf7ece2fd8944bb80354e25282ea diff --git a/src/server.ts b/src/server.ts index 6c94532..e9915d3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,88 +5,200 @@ import { v4 as uuidv4 } from 'uuid'; 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', 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); return; } - if(!view) { - res.send(output); - return; + if(view) { + const viewOutput = view(output); + if(viewOutput.length) { + res.send(viewOutput); + return; + } } - const viewOutput = view(output); - if(viewOutput.length) { - res.send(viewOutput); + if(!output) { + res.status(204); return; } - res.status(204); + return res.json(output); } catch(e) { console.error(e); res.status(500); } + finally { + res.end(); + } }); } -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, options: WrapOptions, fn: WrappedApiHandler, view?: (arr: any) => string): void { + apiWrapper('delete', endpoint, options, fn, view); } -apiPost('/feeds', 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 `Your login code has been emailed to you.`; +}); + +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 => { + if(data) { + return data.html.replace(/{ACCOUNT_ID}/g, data.account_id); + } + else { + return data; + } +}); + +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; - const rss = new RSSParser(); - const feedData = await rss.parse(url); + let parser: BaseParser; + + // based on the url, we should figure out if this is a reddit or rss feed + parser = new RSSParser(); + + 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 } -}, (feed: any): string => { - return ''; }); -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); @@ -99,7 +211,8 @@ apiGet('/feeds', async (req, res): Promise => { }); return { - feeds: feedsWithUnread + feeds: feedsWithUnread, + account_id: account_id } }, (output: any): string => { const feeds = output.feeds; @@ -107,7 +220,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")}`; @@ -119,26 +232,65 @@ function reasonable(date: Date): string { return `${date.getFullYear()}-${month}-${day}`; } -apiGet('/feeds/:feed_id/items', 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 => { + return ` + Feed: ${feed.title}
    + Link: ${feed.link}
    + `; +}); - return query.getFeedsFor.all(id, 0); +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(feed_id, 0), + info: query.getFeedInfo.get(feed_id), + account_id + } }, (feedData: any): string => { return ` - ${feedData.map((item: any, index: number) => { +
    + + Feed: ${feedData.info.title}
    +
    +
    +
    TitlePublish Date
    +
    + `; }); -apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => { - const feed_id = req.params.feed_id; - const item_id = req.params.item_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('/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); @@ -152,8 +304,19 @@ apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => { Read: ${new Date(output.read_at || undefined)}
    ${output.content}
    - ` - return output.content; + `; +}); + +apiDelete('/accounts/:account_id/feeds/:feed_id',{auth: true}, async (req, res) => { + const { feed_id, account_id } = req.params; + if(!query.isFeedOwnedBy(account_id, feed_id)) { + throw new Error('Invalid feed'); + } + + query.deleteFeed.run(feed_id); + console.log(`Deleting feed ${feed_id}`); + res.setHeader('HX-Trigger', 'newFeed'); + return; }); async function periodicIngest() {