Archive commit

This commit is contained in:
Erik McClure
2019-08-28 21:15:02 -07:00
commit 735845746e
1435 changed files with 140724 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
import { Router } from 'express';
import { offline as createOffline, validAccount as createValidAccount, hash, wrap, limit } from '../requestUtils';
import {
createUpdateAccount, createRemoveSite, createUpdateSettings, createGetAccountCharacters, removeHide, getHides, getFriends
} from '../api/account';
import { findAccountSafe, findAuth, findAllCharacters, countAllVisibleAuths, IAccount } from '../db';
import { system } from '../logger';
import { Settings, ServerConfig } from '../../common/adminInterfaces';
export default function (server: ServerConfig, settings: Settings) {
const validAccount = createValidAccount(server);
const offline = createOffline(settings);
const app = Router();
const getAccountCharacters = createGetAccountCharacters(findAllCharacters);
const updateAccount = createUpdateAccount(findAccountSafe, system);
const updateSettings = createUpdateSettings(findAccountSafe);
const removeSite = createRemoveSite(findAuth, countAllVisibleAuths, system);
app.post('/account-characters', offline, hash, validAccount, limit(60, 60), wrap(server, req =>
getAccountCharacters(req.user as IAccount)));
app.post('/account-update', offline, hash, validAccount, limit(60, 60), wrap(server, req =>
updateAccount(req.user as IAccount, req.body.account)));
app.post('/account-settings', offline, hash, validAccount, limit(60, 60), wrap(server, req =>
updateSettings(req.user as IAccount, req.body.settings)));
app.post('/remove-site', offline, hash, validAccount, limit(60, 60), wrap(server, req =>
removeSite(req.user as IAccount, req.body.siteId)));
app.post('/remove-hide', offline, hash, validAccount, limit(60, 60), wrap(server, req =>
removeHide(req.user as IAccount, req.body.hideId)));
app.post('/get-hides', offline, hash, validAccount, limit(60, 60), wrap(server, req =>
getHides(req.user as IAccount, req.body.page || 0)));
app.post('/get-friends', offline, hash, validAccount, limit(120, 60), wrap(server, req =>
getFriends(req.user as IAccount)));
return app;
}
+35
View File
@@ -0,0 +1,35 @@
import { Router } from 'express';
import { limit, offline as createOffline, wrap, validAccount as createValidAccount, hash } from '../requestUtils';
import { createJoinGame, Config } from '../api/game';
import { findServer } from '../internal';
import { findCharacter, hasActiveSupporterInvites, IAccount } from '../db';
import { createJoin } from '../internal';
import { Settings, ServerConfig } from '../../common/adminInterfaces';
import { getOrigin, addOrigin } from '../originUtils';
export default function (server: ServerConfig, settings: Settings, config: Config) {
const offline = createOffline(settings);
const validAccount = createValidAccount(server);
const join = createJoin();
const app = Router();
let inQueue = 0;
const joinGame = createJoinGame(findServer, config, findCharacter, join, addOrigin, hasActiveSupporterInvites);
app.post('/game/join', offline, limit(60, 5 * 60), hash, validAccount, wrap(server, async req => {
if (inQueue > 100) {
return {};
} else {
try {
inQueue++;
const { ponyId, serverId, version, url, alert } = req.body;
return await joinGame(req.user as IAccount, ponyId, serverId, version, url, alert, getOrigin(req));
} finally {
inQueue--;
}
}
}));
return app;
}
+37
View File
@@ -0,0 +1,37 @@
import { Router } from 'express';
import { offline as createOffline, validAccount as createValidAccount, hash, wrap } from '../requestUtils';
import { createFromRequest } from '../reporter';
import { createSavePony, createRemovePony } from '../api/pony';
import { findAuth, findCharacter, characterCount, createCharacter, removeCharacter, IAccount } from '../db';
import { updateCharacterCount } from '../accountUtils';
import { system } from '../logger';
import { kickFromAllServersByCharacter } from '../api/admin';
import { createIsSuspiciousName, createIsSuspiciousPony } from '../../common/security';
import { Settings, ServerConfig } from '../../common/adminInterfaces';
import { RemovedDocument } from '../internal';
import { logRemovedCharacter } from '../characterUtils';
export default function (server: ServerConfig, settings: Settings, removedDocument: RemovedDocument) {
const offline = createOffline(settings);
const validAccount = createValidAccount(server);
const app = Router();
const isSuspiciousName = createIsSuspiciousName(settings);
const isSuspiciousPony = createIsSuspiciousPony(settings);
const savePonyHandler = createSavePony(
findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, system,
isSuspiciousName, isSuspiciousPony);
const removePonyHandler = createRemovePony(
kickFromAllServersByCharacter, removeCharacter, updateCharacterCount,
id => removedDocument('ponies', id), logRemovedCharacter);
app.post('/pony/save', offline, hash, validAccount, wrap(server, req =>
savePonyHandler(req.user as IAccount, req.body.pony, createFromRequest(server, req))));
app.post('/pony/remove', offline, hash, validAccount, wrap(server, req =>
removePonyHandler(req.body.id, (req.user as IAccount).id)));
return app;
}
+95
View File
@@ -0,0 +1,95 @@
import { Router } from 'express';
import * as fs from 'fs';
import * as path from 'path';
import { repeat } from 'lodash';
import { randomString } from '../../common/stringUtils';
import { execAsync } from '../serverUtils';
import { offline as createOffline, handleJSON, auth } from '../requestUtils';
import * as paths from '../paths';
import { findAllCharacters, IAccount } from '../db';
import { createGetAccountCharacters } from '../api/account';
import { Settings, ServerConfig } from '../../common/adminInterfaces';
import { World } from '../world';
import { ToolsMapInfo } from '../../components/tools/tools-map/tools-map';
import { flatten } from '../../common/utils';
import { serializeMap } from '../serverMap';
export default function (server: ServerConfig, settings: Settings, world: World | undefined) {
const offline = createOffline(settings);
const app = Router();
app.use(auth);
app.get('/ponies', offline, (req, res) => {
handleJSON(server, req, res, createGetAccountCharacters(findAllCharacters)(req.user as IAccount));
});
app.get('/animation/:id', offline, (req, res) => {
const filePath = path.join(paths.store, req.params.id);
res.sendFile(filePath);
});
app.post('/animation', offline, (req, res) => {
const name = randomString(10);
const filePath = path.join(paths.store, name);
fs.writeFileAsync(filePath, req.body.animation, 'utf8')
.then(() => res.send({ name }));
});
app.post('/animation-gif', offline, (req, res) => {
const image: string = req.body.image;
const width: number = req.body.width || 80;
const height: number = req.body.height || 80;
const fps: number = req.body.fps || 24;
const remove: number = req.body.remove || 0;
const name = randomString(10);
const filePath = path.join(paths.store, name + '.png');
const header = 'data:image/gif;base64,';
const buffer = Buffer.from(image.substr(header.length), 'base64');
const magick = /^win/.test(process.platform) ? 'magick' : 'convert';
const command = `${magick} -dispose 3 -delay ${100 / fps} -loop 0 "${filePath}" -crop ${width}x${height} `
+ `+repage${repeat(' +delete', remove)} "${filePath.replace(/png$/, 'gif')}"`;
fs.writeFileAsync(filePath, buffer)
.then(() => execAsync(command))
.then(() => res.send({ name }));
});
app.get('/maps', offline, (_, res) => {
if (world) {
res.json(world.maps.map(m => m.id));
} else {
res.sendStatus(400);
}
});
app.get('/map', offline, (req, res) => {
if (world) {
const id = req.query.map || '';
const map = world.maps.find(m => m.id === id);
if (map) {
const mapInfo: ToolsMapInfo = {
...serializeMap(map),
defaultTile: map.defaultTile,
type: map.type,
info: {
season: world.season,
entities: flatten(map.regions.map(r => r.entities))
.map(({ type, x, y, order, id }) => ({ type, x, y, order, id })),
},
};
res.json(mapInfo);
return;
}
}
res.sendStatus(400);
});
return app;
}
+20
View File
@@ -0,0 +1,20 @@
import { Router } from 'express';
import { auth } from '../requestUtils';
import { Settings, ServerConfig } from '../../common/adminInterfaces';
import { RemovedDocument } from '../internal';
import { Config } from '../api/game';
import apiAccount from './api-account';
import apiPony from './api-pony';
import apiGame from './api-game';
export default function (server: ServerConfig, settings: Settings, config: Config, removedDocument: RemovedDocument) {
const app = Router();
app.use(auth);
app.use(apiAccount(server, settings));
app.use(apiPony(server, settings, removedDocument));
app.use(apiGame(server, settings, config));
return app;
}
+60
View File
@@ -0,0 +1,60 @@
import { Router } from 'express';
import { noop } from 'lodash';
import { findAllCharacters, findAllVisibleAuths, IAccount, Account } from '../db';
import { offline, hash, handleJSON } from '../requestUtils';
import { createGetAccountData } from '../api/account';
import { Settings, ServerConfig } from '../../common/adminInterfaces';
import { includes } from '../../common/utils';
const blockApps: string[] = [];
const MAX_CONCURRENT_REQUESTS = 100;
let requests = 0;
export default function (server: ServerConfig, settings: Settings) {
const app = Router();
const getAccountData = createGetAccountData(findAllCharacters, findAllVisibleAuths);
async function handleAccountRequest(account: IAccount, userAgent?: string, browserId?: string) {
if (requests < MAX_CONCURRENT_REQUESTS) {
requests++;
try {
const lastUserAgent = userAgent || account.lastUserAgent;
const lastBrowserId = browserId || account.lastBrowserId;
if ((lastUserAgent && account.lastUserAgent !== lastUserAgent) ||
(lastBrowserId && account.lastBrowserId !== lastBrowserId)) {
account.lastUserAgent = lastUserAgent;
account.lastBrowserId = lastBrowserId;
Account.updateOne({ _id: account._id }, { lastUserAgent, lastBrowserId }, noop);
}
return await getAccountData(account);
} finally {
requests--;
}
} else {
return { limit: true };
}
}
app.post('/account', offline(settings), hash, (req, res) => {
req.session!.touch();
let account = req.user as IAccount | undefined;
const browserId = req.get('Api-Bid');
const userAgent = req.get('User-Agent') || '';
const requestedWith = req.get('X-Requested-With');
const isWebViewUserAgent = /Chrome\/\d+\.0\.0\.0 Mobile|; wv\)/.test(userAgent);
const isWebView = requestedWith || isWebViewUserAgent;
if (!account || (settings.blockWebView && isWebView && includes(blockApps, requestedWith))) {
handleJSON(server, req, res, null);
} else {
handleJSON(server, req, res, handleAccountRequest(account, userAgent, browserId));
}
});
return app;
}
+71
View File
@@ -0,0 +1,71 @@
import { Router } from 'express';
import { GameStatus, ServerInfo, ServerInfoShort } from '../../common/interfaces';
import { InternalGameServerState, Settings, ServerLiveSettings } from '../../common/adminInterfaces';
import { offline } from '../requestUtils';
import { servers } from '../internal';
import { version } from '../config';
import { isServerOffline } from '../serverUtils';
import { StatsTracker } from '../stats';
import { MIN_ADULT_AGE } from '../../common/constants';
function isServerSafe(server: InternalGameServerState) {
return server.state.alert !== '18+';
}
function toServerState(server: InternalGameServerState): ServerInfo {
const { name, path, desc, flag, alert, online, settings, require, host } = server.state;
return {
id: server.id,
name,
path,
desc,
host,
flag,
alert,
dead: false,
online,
offline: isServerOffline(server),
filter: !!settings.filterSwears,
require,
};
}
function toServerStateShort(server: InternalGameServerState): ServerInfoShort {
return {
id: server.id,
online: server.state.online,
offline: isServerOffline(server),
};
}
function getGameStatus(
servers: InternalGameServerState[], live: ServerLiveSettings, short: boolean, age: number
): GameStatus {
const adult = age >= MIN_ADULT_AGE;
return {
version,
update: live.updating ? true : undefined,
servers: servers
.filter(s => isServerSafe(s) || adult)
.map(short ? toServerStateShort : toServerState),
};
}
export default function (settings: Settings, live: ServerLiveSettings, statsTracker: StatsTracker) {
const app = Router();
app.get('/game/status', offline(settings), (req, res) => {
const status = getGameStatus(servers, live, req.query.short === 'true', req.query.d | 0);
res.json(status);
statsTracker.logRequest(req, status);
});
app.post('/csp', offline(settings), (_, res) => {
//logger.warn('CSP report', getIPFromRequest(req), req.body['csp-report']);
res.sendStatus(200);
});
return app;
}
+317
View File
@@ -0,0 +1,317 @@
import { Router, Request, Response, RequestHandler } from 'express';
import { use, authenticate, AuthenticateOptions } from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { remove } from 'lodash';
import { MINUTE } from '../../common/constants';
import { fromNow, hasFlag, includes } from '../../common/utils';
import { BannedMuted, Settings, ServerConfig, AccountFlags, ServerLiveSettings } from '../../common/adminInterfaces';
import { Account, IAccount, Origin, IOrigin } from '../db';
import { limit, auth as authRequest, wrap } from '../requestUtils';
import { CreateAccountOptions, findOrCreateAccount, SuspiciousCheckers, getAccountAlertMessage } from '../accountUtils';
import { create, createFromRequest } from '../reporter';
import { logger, logServer, system } from '../logger';
import { providers, getProfile } from '../oauth';
import { accountChanged, RemovedDocument } from '../internal';
import { UserError, isUserError, reportUserError } from '../userError';
import { kickFromAllServers } from '../api/admin';
import { createIsSuspiciousName, createIsSuspiciousAuth } from '../../common/security';
import { isBanned, isActive } from '../../common/adminUtils';
import { mergeAccounts } from '../api/merge';
import { findOrCreateAuth } from '../authUtils';
import { getOriginFromHTTP, getOrigin, addOrigin } from '../originUtils';
import { Profile } from '../../common/interfaces';
interface MergeRequest {
accountId: string;
time: number;
}
const FRESH_ACCOUNT_TIME = 1 * MINUTE;
const mergeRequests: MergeRequest[] = [];
/* tslint:disable */
const ignoreErrors = [
'Service unavailable', // replacement for twitter HTTP error
'Internal error',
'User denied your request',
'Code was already redeemed.',
'Code is invalid or expired.',
'This authorization code has expired.',
'This authorization code has been used.',
'Failed to fetch user profile',
'Failed to obtain access token',
'Failed to find request token in session',
'User authorization failed: user is deactivated.',
'User authorization failed: user revoke access for this token.',
'Backend Error',
'TokenError',
'Bad Request',
'Rate limit exceeded',
`Sorry, this feature isn't available right now: An error occurred while processing this request. Please try again later.`,
'Przepraszamy, ta funkcja nie jest obecnie dostępna: Podczas przetwarzania żądania wystąpił błąd. Spróbuj ponownie później.',
'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже',
'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже.',
'Desculpe, esse recurso não está disponível no momento: Ocorreu um erro ao processar essa solicitação. Tente novamente mais tarde.',
'Esta aplicación no está disponible: La aplicación que intentas usar ya no está disponible o tiene el acceso restringido.',
'Xin lỗi, tính năng này không khả dụng ngay bây giờ: Đã xảy ra lỗi khi xử lý yêu cầu này. Vui lòng thử lại sau.',
'Lo sentimos, esta función no está disponible ahora: Ocurrió un error mientras se procesaba la solicitud. Vuelve a intentarlo más tarde.',
`The access token is invalid since the user hasn't engaged the app in longer than 90 days.`,
`Application Unavailable: The application you're trying to use is either no longer available or access is restricted.`,
'An unexpected error has occurred. Please retry your request later.',
'Code was invalid or expired. ',
'Internal server error: could not check access_token now, check later.',
'failed to fetch user profile',
'Failed to obtain request token',
'User canceled the Dialog flow',
'Internal Error',
'Bad Authentication data.',
'Diese Function ist vorübergehend nicht verfügbar',
'Diese Funktion ist vorübergehend nicht verfügbar',
'User authorization failed: no access_token passed.',
'Ungültiges Anfrage-Token.',
'Invalid Credentials',
'Invalid code.',
'Internal server error: Database problems, try later',
'An invalid Platform session was found.: An invalid Platform session was found.',
`Cannot read property 'id' of undefined`, // patreon error
'User Rate Limit Exceeded. Rate of requests for user exceed configured project quota. You may consider re-evaluating expected per-user traffic to the API and adjust project quota limits accordingly. You may monitor aggregate quota usage and adjust limits in the API Console: https://console.developers.google.com/apis/api/plus.googleapis.com/quotas?project=200390553857',
];
function kickCurrentUser(req: Request) {
const user = req.user as IAccount | undefined;
if (user) {
kickFromAllServers(user.id)
.catch(e => logger.error(e));
}
}
function logIn(req: Request, account: IAccount) {
return new Promise<void>((resolve, reject) => {
kickCurrentUser(req);
req.logIn(account, e => e ? reject(e) : resolve());
});
}
function isMerge(accountId: string) {
const minTime = fromNow(-10 * MINUTE).getTime();
remove(mergeRequests, r => r.time < minTime);
return mergeRequests.some(r => r.accountId === accountId);
}
function getIP(req: Request) {
return req.ip || req.ips[0];
}
function reportError(server: ServerConfig, message: string, e: Error, req: Request) {
createFromRequest(server, req).danger(message, e.toString());
logger.error(message, e);
}
function fixTwitterErrorMessage(message: string) {
return /^<!DOCTYPE html>/.test(message) ? 'Service unavailable' : message;
}
async function checkBanField(
server: ServerConfig, account: IAccount, field: keyof BannedMuted, message: string, origin: IOrigin
) {
if (isActive(origin[field]) && !isActive(account[field])) {
create(server, account._id, undefined, origin).warn(message);
account[field] = origin[field];
await account.save();
}
}
async function loginUser(server: ServerConfig, req: Request, res: Response, account: IAccount) {
const origin = await Origin.findOne({ ip: getIP(req) }).exec();
await addOrigin(account, getOrigin(req));
if (origin) {
await checkBanField(server, account, 'mute', 'Muted account by origin', origin);
await checkBanField(server, account, 'shadow', 'Shadowed account by origin', origin);
await checkBanField(server, account, 'ban', 'Banned account by origin', origin);
}
if (isBanned(account)) {
// const message = isTemporarilyBanned(account) ? `Account locked()` : 'Account locked';
throw new UserError('Account locked', undefined, getAccountAlertMessage(account));
}
await logIn(req, account);
await accountChanged(account._id.toString());
const isFresh = account.createdAt && account.createdAt.getTime() > fromNow(-FRESH_ACCOUNT_TIME).getTime();
res.redirect(isFresh ? '/account' : '/');
}
async function mergeUser(req: Request, res: Response, account: IAccount, removedDocument: RemovedDocument) {
const user = req.user as IAccount;
const userId = user._id.toString();
const accountId = account._id.toString();
remove(mergeRequests, r => r.accountId === userId);
if (userId !== accountId) {
await mergeAccounts(userId, accountId, 'by user', removedDocument, false);
}
res.redirect('/account?merged=true');
}
function handleErrorAndRedirect(
server: ServerConfig, url: string, message: string, e: Error, req: Request, res: Response
) {
if (isUserError(e)) {
reportUserError(e, server, req);
url += `?error=${encodeURIComponent(e.message)}`;
if (e.userInfo) {
url += `&alert=${encodeURIComponent(e.userInfo)}`;
}
} else {
reportError(server, `Auth error: ${message}`, e, req);
url += `?error=${encodeURIComponent(message)}`;
}
res.redirect(url);
}
async function handleAuth(
server: ServerConfig, live: ServerLiveSettings, removedDocument: RemovedDocument,
req: Request, res: Response, error: Error | null, account: IAccount | null,
) {
const user = req.user as IAccount | undefined;
const merge = isMerge(user && user.id);
try {
if (error) {
if (isUserError(error)) {
throw error;
}
const message = fixTwitterErrorMessage(error.message);
const ignore = includes(ignoreErrors, message);
throw new UserError(message, ignore ? undefined : { error, desc: `url: ${req.path}` });
}
if (!account) {
throw new UserError('No account');
}
if (merge && !hasFlag(account.flags, AccountFlags.BlockMerging)) {
if (live.shutdown) {
throw new Error(`Cannot merge while server is shutdown`);
}
await mergeUser(req, res, account, removedDocument);
} else {
await loginUser(server, req, res, account);
}
} catch (e) {
const message = merge ? 'Account merge error' : 'Authentication error';
handleErrorAndRedirect(server, merge ? '/account' : '/', message, e, req, res);
}
}
function createHandler(
server: ServerConfig, live: ServerLiveSettings, id: string, options: AuthenticateOptions,
removedDocument: RemovedDocument
): RequestHandler {
return (req, res, next) => {
const handler = authenticate(id, options, (error: Error | null, account: IAccount | null) =>
handleAuth(server, live, removedDocument, req, res, error, account));
return handler(req, res, next);
};
}
export function authRoutes(
host: string, server: ServerConfig, settings: Settings, live: ServerLiveSettings, mockLogin: boolean,
removedDocument: RemovedDocument
) {
const failureRedirect = `/?error=${encodeURIComponent('Authentication failed')}`;
const app = Router();
const checkers: SuspiciousCheckers = {
isSuspiciousName: createIsSuspiciousName(settings),
isSuspiciousAuth: createIsSuspiciousAuth(settings),
};
providers.filter(p => !!p.auth).forEach(({ id, strategy, auth, connectOnly, additionalOptions = {} }) => {
const callbackURL = `${host}auth/${id}/callback`;
const scope = id === 'patreon' ? ['users'] : ['email'];
const options = {
...additionalOptions,
...auth,
callbackURL,
includeEmail: true,
profileFields: ['id', 'displayName', 'name', 'emails'],
passReqToCallback: true,
};
async function signInOrSignUp(req: Request, profile: Profile) {
const user = req.user as IAccount | undefined;
const userId = user && user._id.toString();
const mergeAccount = (userId && isMerge(userId)) ? userId : undefined;
const createAccountOptions = createOptions(req, !!connectOnly, server, settings, checkers);
const auth = await findOrCreateAuth(profile, mergeAccount, createAccountOptions);
const account = await findOrCreateAccount(auth, profile, createAccountOptions);
const { ip, userAgent } = createAccountOptions;
system(account._id, `signed-in with "${auth.name}" [${auth._id}] [${ip}] [${userAgent}]`);
return account;
}
use(id, new strategy(options, (req, _accessToken, _refreshToken, oauthProfile, callback) => {
const profile = getProfile(id, oauthProfile);
signInOrSignUp(req, profile)
.then(account => {
callback(null, account);
})
.catch((error: Error) => {
logServer(`failed to sign-in ${JSON.stringify(profile)}`);
callback(error, null);
});
}));
app.get(`/${id}`, limit(120, 3600), createHandler(server, live, id, { scope, failureRedirect }, removedDocument));
app.get(`/${id}/callback`, limit(120, 3600), createHandler(server, live, id, { failureRedirect }, removedDocument));
app.get(`/${id}/merge`, limit(120, 3600), authRequest, (req, res) => {
const accountId = (req.user as IAccount)._id.toString();
mergeRequests.push({ accountId, time: Date.now() });
res.redirect(`/auth/${id}`);
});
});
app.post('/sign-out', wrap(server, req => {
kickCurrentUser(req);
req.logout();
return { success: true };
}));
if (mockLogin) {
use(new LocalStrategy((login, _pass, done) => Account.findById(login, done)));
app.get('/local', authenticate('local', { successRedirect: '/', failureRedirect: '/failed-login' }));
}
return app;
}
function createOptions(
req: Request, connectOnly: boolean, server: ServerConfig, settings: Settings, checkers: SuspiciousCheckers
): CreateAccountOptions {
const acl = req.cookies && req.cookies.acl;
const origin = getOriginFromHTTP(req);
return {
ip: getIP(req),
userAgent: req.get('User-Agent'),
browserId: req.get('Api-Bid'),
connectOnly: !!connectOnly,
creationLocked: acl && acl > (new Date()).toISOString(),
canCreateAccounts: !!settings.canCreateAccounts,
reportPotentialDuplicates: !!settings.reportPotentialDuplicates,
warn: (accountId, message, desc) => create(server, accountId, undefined, origin).warn(message, desc),
...checkers,
};
}
+167
View File
@@ -0,0 +1,167 @@
import * as fs from 'fs';
import * as path from 'path';
import { compileFile } from 'pug';
import { RequestHandler } from 'express';
import { ClientOptions, Server, writeObject } from 'ag-sockets';
import { OAuthProvider } from '../../common/interfaces';
import { providers, OAuthProviderInfo } from '../oauth';
import { config, version, description } from '../config';
import { TokenData } from '../serverInterfaces';
import { logger } from '../logger';
import { pathTo } from '../paths';
import { writeBinary } from '../../common/binaryUtils';
interface RevFile {
name: string;
path: string;
url: string;
}
interface PageOptions {
isPublic?: boolean;
production: boolean;
base: string;
assets?: string;
style: string;
script: string;
scriptES: string;
token?: string;
noindex?: boolean;
socketOptions?: ClientOptions;
webpack?: boolean;
local?: boolean;
}
function getFiles(urlBase: string, dir: string, sub: string): RevFile[] {
try {
return fs.readdirSync(path.join(dir, sub))
.filter(file => /\.(js|css|png)$/.test(file))
.map(file => ({
name: file.replace(/-[a-f0-9]{10}\.(js|css|png)$/, '.$1'),
path: path.join(dir, sub, file),
url: `${urlBase}/${sub}/${file}`,
}));
} catch {
return [];
}
}
export function createIndex(assetsPath: string, adminAssetsPath: string) {
function toOAuthProvider({ id, name, color, auth, connectOnly }: OAuthProviderInfo): OAuthProvider {
return { id, name, color, disabled: auth ? undefined : true, connectOnly };
}
const revServer = new Map<string, RevFile>();
[
...getFiles('assets', assetsPath, 'styles'),
...getFiles('assets', assetsPath, 'scripts'),
...getFiles('assets', assetsPath, 'images'),
...getFiles('assets-admin', adminAssetsPath, 'styles'),
...getFiles('assets-admin', adminAssetsPath, 'scripts'),
].forEach(file => revServer.set(file.name, file));
function revUrlGetter(dir: string) {
return (name: string) => {
const file = revServer.get(name);
return file && file.url || `assets/${dir}/${name}`;
};
}
function getRevPath(name: string) {
return (revServer.get(name) && revServer.get(name)!.path) || path.join(assetsPath, name);
}
const getRevScriptURL = revUrlGetter('scripts');
const getRevStyleURL = revUrlGetter('styles');
const getRevImageURL = revUrlGetter('images');
const template = compileFile(pathTo('views', 'index.pug'));
const inlineStyle = fs.readFileSync(getRevPath('style-inline.css'), 'utf8');
const loadingImage = fs.readFileSync(getRevPath('logo-gray.png'));
const oauthProviders = providers.map(toOAuthProvider);
function encodeSocketOptions(options: ClientOptions | undefined): string {
if (options) {
const data = writeBinary(writer => writeObject(writer, options));
const buffer = Buffer.from(data);
return buffer.toString('base64');
} else {
return '';
}
}
function renderPage(
{ isPublic, style, script, scriptES, production, noindex, base, socketOptions, token, local }: PageOptions
) {
return template({
doctype: 'html',
host: config.host,
title: config.title,
twitterLink: config.twitterLink,
supporterLink: config.supporterLink,
email: config.contactEmail,
logo: `${config.host}${getRevImageURL('logo-120.png')}`,
loadingImage: `data:image/png;base64,${loadingImage.toString('base64')}`,
version,
description,
base,
token,
sw: config.sw ? 'true' : undefined,
noindex: noindex || config.noindex,
production,
local: local ? 'true' : undefined,
socketOptions: encodeSocketOptions(socketOptions),
inlineStyle,
style,
script,
scriptES,
oauthProviders,
facebookAppId: config.facebookAppId,
isPublic: isPublic ? 'true' : undefined,
});
}
function admin(
production: boolean, base: string, assetsBase: string, scriptName: string, socket: Server
): RequestHandler {
const socketOptions = socket.options();
const style = `${assetsBase}/${getRevStyleURL('style-admin.css')}`;
const script = `${assetsBase}/${getRevScriptURL(scriptName)}`;
const scriptES = script;
return (req, res) => {
try {
const token = socket.token({ account: req.user } as TokenData);
res.send(renderPage({ production, base, style, script, scriptES, noindex: true, socketOptions, token }));
} catch (e) {
logger.error(e);
res.sendStatus(500);
}
};
}
function user(
production: boolean, base: string, styleName: string, scriptName: string, scriptESName: string,
socketOptions: ClientOptions | undefined, noindex: boolean, local: boolean, isPublic: boolean,
) {
const style = `/${getRevStyleURL(styleName)}`;
const script = `/${getRevScriptURL(scriptName)}`;
const scriptES = `/${getRevScriptURL(scriptESName)}`;
const sprites1 = DEVELOPMENT ? `/assets/images/pony.png` : `/${getRevImageURL('pony.png')}`;
const sprites2 = DEVELOPMENT ? `/assets/images/pony2.png` : `/${getRevImageURL('pony2.png')}`;
const page = renderPage({ isPublic, production, base, style, script, scriptES, socketOptions, noindex, local });
const preload = [
`<${script}>; rel=preload; as=script`,
`<${style}>; rel=preload; as=style`,
`<${sprites1}>; rel=preload; as=fetch; crossorigin`,
`<${sprites2}>; rel=preload; as=fetch; crossorigin`,
];
return { page, preload };
}
return { admin, user, getRevScript: getRevScriptURL, getRevStyle: getRevStyleURL };
}