mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-25 06:05:52 +02:00
Archive commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user