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<string, string> = new Map<string, string>();
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<any>;
+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);
}
}
- res.status(204);
+ if(!output) {
+ res.status(204);
+ return;
+ }
+
+ return res.json(output);
}
catch(e) {
console.error(e);
});
}
-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<any> => {
+apiPost('/login', {auth: false}, async (req, res): Promise<any> => {
+ 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<any> => {
+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<any> => {
// get info about the feed
const url = req.body.link;
+ const account_id = req.params.account_id;
let parser: BaseParser;
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<any> => {
- const feeds = query.getFeedList.all();
+apiGet('/accounts/:account_id/feeds', {auth: true}, async (req, res): Promise<any> => {
+ 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);
});
return {
- feeds: feedsWithUnread
+ feeds: feedsWithUnread,
+ account_id: account_id
}
}, (output: any): string => {
const feeds = output.feeds;
const display = feed.unread ? `(${feed.unread})` : '';
const first = idx === 0;
- return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#list-pane" data-feed-id="${feed.id}">${feed.title}
+ return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/accounts/${output.account_id}/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#list-pane" data-feed-id="${feed.id}">${feed.title}
<span class="unread-count">${display}</span>
</a></li>`
}).join("\n")}</ul>`;
return `${date.getFullYear()}-${month}-${day}`;
}
-apiGet('/feeds/:feed_id', async (req, res): Promise<any> => {
+apiGet('/accounts/:account_id/feeds/:feed_id',{auth: true}, async (req, res): Promise<any> => {
const id = req.params.feed_id;
return query.getFeedInfo.get(id);
}, (feed: any): string => {
`;
});
-apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
- const id = req.params.feed_id;
+apiGet('/accounts/:account_id/feeds/:feed_id/items',{auth: true}, async (req, res): Promise<any> => {
+ 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 `
<div id="feed-info">
<div id="feed-actions">
- <a href="#" class="btn" hx-post="/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
- <a href="#" class="btn" hx-delete="/feeds/${feedData.info.id}" hx-trigger="click" hx-confirm="Are you sure you want to delete this feed?">Delete Feed</a>
+ <a href="#" class="btn" hx-post="/accounts/${feedData.account_id}/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
+ <a href="#" class="btn" hx-delete="/accounts/${feedData.account_id}/feeds/${feedData.info.id}" hx-trigger="click" hx-confirm="Are you sure you want to delete this feed?">Delete Feed</a>
</div>
<b>Feed: </b>${feedData.info.title}<br>
</div>
const read = !!item.read_at;
const first = index === 0;
return `<li>
- <a href="#" class="${first ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/feeds/${item.feed_id}/items/${item.id}" hx-trigger="click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
+ <a href="#" class="${first ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/accounts/${feedData.account_id}/feeds/${item.feed_id}/items/${item.id}" hx-trigger="click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
<span class="date">${reasonable(new Date(item.pub_date * 1000))}</span>
</a>
</li>
`;
});
-apiPost('/feeds/:feed_id/items/markAsRead', async (req, res): Promise<any> => {
- 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<any> => {
+ 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);
<b>Read: </b> ${new Date(output.read_at || undefined)}
</div>
<div class="scrollable" id="feed-content">${output.content}</div>
- `
- 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);