feat: headless mode
This commit is contained in:
parent
231f734fd5
commit
6f3081cb8e
7 changed files with 1749 additions and 848 deletions
|
@ -102,6 +102,8 @@ export interface ConfigFeatures {
|
||||||
|
|
||||||
oauth_registration: boolean;
|
oauth_registration: boolean;
|
||||||
user_registration: boolean;
|
user_registration: boolean;
|
||||||
|
|
||||||
|
headless: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigOAuth {
|
export interface ConfigOAuth {
|
||||||
|
|
|
@ -138,6 +138,8 @@ export default function readConfig() {
|
||||||
map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'),
|
map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'),
|
||||||
map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'),
|
map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'),
|
||||||
|
|
||||||
|
map('FEATURES_HEADLESS', 'boolean', 'features.headless'),
|
||||||
|
|
||||||
map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'),
|
map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'),
|
||||||
map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'),
|
map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'),
|
||||||
|
|
||||||
|
|
|
@ -166,12 +166,14 @@ const validator = s.object({
|
||||||
invites_length: s.number.default(6),
|
invites_length: s.number.default(6),
|
||||||
oauth_registration: s.boolean.default(false),
|
oauth_registration: s.boolean.default(false),
|
||||||
user_registration: s.boolean.default(false),
|
user_registration: s.boolean.default(false),
|
||||||
|
headless: s.boolean.default(false),
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
invites: false,
|
invites: false,
|
||||||
invites_length: 6,
|
invites_length: 6,
|
||||||
oauth_registration: false,
|
oauth_registration: false,
|
||||||
user_registration: false,
|
user_registration: false,
|
||||||
|
headless: false,
|
||||||
}),
|
}),
|
||||||
chunks: s
|
chunks: s
|
||||||
.object({
|
.object({
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { OauthProviders } from '@prisma/client';
|
import { OauthProviders } from '@prisma/client';
|
||||||
|
import config from 'lib/config';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import { createToken } from 'lib/util';
|
import { createToken } from 'lib/util';
|
||||||
|
@ -30,6 +31,12 @@ export const withOAuth =
|
||||||
const logger = Logger.get(`oauth::${provider}`);
|
const logger = Logger.get(`oauth::${provider}`);
|
||||||
|
|
||||||
function oauthError(error: string) {
|
function oauthError(error: string) {
|
||||||
|
if (config.features.headless)
|
||||||
|
return res.json({
|
||||||
|
error,
|
||||||
|
provider,
|
||||||
|
});
|
||||||
|
|
||||||
return res.redirect(`/oauth_error?error=${error}&provider=${provider}`);
|
return res.redirect(`/oauth_error?error=${error}&provider=${provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,18 @@ export const withZipline =
|
||||||
|
|
||||||
req.user = async () => {
|
req.user = async () => {
|
||||||
try {
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader) {
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
token: authHeader,
|
||||||
|
},
|
||||||
|
include: { oauth: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) return user;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = req.getCookie('user');
|
const userId = req.getCookie('user');
|
||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
|
|
||||||
|
|
|
@ -81,12 +81,17 @@ async function start() {
|
||||||
const handle = nextServer.getRequestHandler();
|
const handle = nextServer.getRequestHandler();
|
||||||
const router = Router({
|
const router = Router({
|
||||||
defaultRoute: (req, res) => {
|
defaultRoute: (req, res) => {
|
||||||
|
if (config.features.headless) {
|
||||||
|
const url = req.url.toLowerCase();
|
||||||
|
if (!url.startsWith('/api') || url === '/api') return notFound(req, res, nextServer);
|
||||||
|
}
|
||||||
|
|
||||||
handle(req, res);
|
handle(req, res);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router.on('GET', '/favicon.ico', async (req, res) => {
|
router.on('GET', '/favicon.ico', async (req, res) => {
|
||||||
if (!existsSync('./public/favicon.ico')) return nextServer.render404(req, res);
|
if (!existsSync('./public/favicon.ico')) return notFound(req, res, nextServer);
|
||||||
|
|
||||||
const favicon = createReadStream('./public/favicon.ico');
|
const favicon = createReadStream('./public/favicon.ico');
|
||||||
res.setHeader('Content-Type', 'image/x-icon');
|
res.setHeader('Content-Type', 'image/x-icon');
|
||||||
|
@ -95,14 +100,14 @@ async function start() {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.on('GET', `${config.urls.route}/:id`, async (req, res, params) => {
|
router.on('GET', `${config.urls.route}/:id`, async (req, res, params) => {
|
||||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
if (params.id === '') return notFound(req, res, nextServer);
|
||||||
|
|
||||||
const url = await prisma.url.findFirst({
|
const url = await prisma.url.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ id: params.id }, { vanity: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
OR: [{ id: params.id }, { vanity: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!url) return nextServer.render404(req, res as ServerResponse);
|
if (!url) return notFound(req, res, nextServer);
|
||||||
|
|
||||||
const nUrl = await prisma.url.update({
|
const nUrl = await prisma.url.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -122,7 +127,7 @@ async function start() {
|
||||||
|
|
||||||
Logger.get('url').debug(`url deleted due to max views ${JSON.stringify(nUrl)}`);
|
Logger.get('url').debug(`url deleted due to max views ${JSON.stringify(nUrl)}`);
|
||||||
|
|
||||||
return nextServer.render404(req, res as ServerResponse);
|
return notFound(req, res, nextServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(res, url.destination);
|
return redirect(res, url.destination);
|
||||||
|
@ -132,8 +137,9 @@ async function start() {
|
||||||
'GET',
|
'GET',
|
||||||
config.uploader.route === '/' ? '/:id' : `${config.uploader.route}/:id`,
|
config.uploader.route === '/' ? '/:id' : `${config.uploader.route}/:id`,
|
||||||
async (req, res, params) => {
|
async (req, res, params) => {
|
||||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
if (params.id === '') return notFound(req, res, nextServer);
|
||||||
else if (params.id === 'dashboard') return nextServer.render(req, res as ServerResponse, '/dashboard');
|
else if (params.id === 'dashboard' && !config.features.headless)
|
||||||
|
return nextServer.render(req, res as ServerResponse, '/dashboard');
|
||||||
|
|
||||||
const image = await prisma.image.findFirst({
|
const image = await prisma.image.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -144,7 +150,7 @@ async function start() {
|
||||||
if (!image) return rawFile(req, res, nextServer, params.id);
|
if (!image) return rawFile(req, res, nextServer, params.id);
|
||||||
else {
|
else {
|
||||||
const failed = await preFile(image, prisma);
|
const failed = await preFile(image, prisma);
|
||||||
if (failed) return nextServer.render404(req, res as ServerResponse);
|
if (failed) return notFound(req, res, nextServer);
|
||||||
|
|
||||||
if (image.password || image.embed || image.mimetype.startsWith('text/'))
|
if (image.password || image.embed || image.mimetype.startsWith('text/'))
|
||||||
redirect(res, `/view/${image.file}`);
|
redirect(res, `/view/${image.file}`);
|
||||||
|
@ -156,7 +162,7 @@ async function start() {
|
||||||
);
|
);
|
||||||
|
|
||||||
router.on('GET', '/r/:id', async (req, res, params) => {
|
router.on('GET', '/r/:id', async (req, res, params) => {
|
||||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
if (params.id === '') return notFound(req, res, nextServer);
|
||||||
|
|
||||||
const image = await prisma.image.findFirst({
|
const image = await prisma.image.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -167,13 +173,17 @@ async function start() {
|
||||||
if (!image) await rawFile(req, res, nextServer, params.id);
|
if (!image) await rawFile(req, res, nextServer, params.id);
|
||||||
else {
|
else {
|
||||||
const failed = await preFile(image, prisma);
|
const failed = await preFile(image, prisma);
|
||||||
if (failed) return nextServer.render404(req, res as ServerResponse);
|
if (failed) return notFound(req, res, nextServer);
|
||||||
|
|
||||||
if (image.password) {
|
if (image.password) {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
res.statusCode = 403;
|
res.statusCode = 403;
|
||||||
return res.end(
|
return res.end(
|
||||||
JSON.stringify({ error: "can't view a raw file that has a password", url: `/view/${image.file}` })
|
JSON.stringify({
|
||||||
|
error: "can't view a raw file that has a password",
|
||||||
|
url: `/view/${image.file}`,
|
||||||
|
code: 403,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} else await rawFile(req, res, nextServer, params.id);
|
} else await rawFile(req, res, nextServer, params.id);
|
||||||
}
|
}
|
||||||
|
@ -202,7 +212,11 @@ async function start() {
|
||||||
|
|
||||||
http.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
http.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||||
|
|
||||||
logger.info(`started ${dev ? 'development' : 'production'} zipline@${version} server`);
|
logger.info(
|
||||||
|
`started ${dev ? 'development' : 'production'} zipline@${version} server${
|
||||||
|
config.features.headless ? ' (headless)' : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
stats(prisma);
|
stats(prisma);
|
||||||
clearInvites(prisma);
|
clearInvites(prisma);
|
||||||
|
@ -211,6 +225,16 @@ async function start() {
|
||||||
setInterval(() => stats(prisma), config.core.stats_interval * 1000);
|
setInterval(() => stats(prisma), config.core.stats_interval * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function notFound(req: IncomingMessage, res: ServerResponse, nextServer: NextServer) {
|
||||||
|
if (config.features.headless) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.end(JSON.stringify({ error: 'not found', url: req.url, code: 404 }));
|
||||||
|
} else {
|
||||||
|
return notFound(req, res, nextServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function preFile(file: Image, prisma: PrismaClient) {
|
async function preFile(file: Image, prisma: PrismaClient) {
|
||||||
if (file.expires_at && file.expires_at < new Date()) {
|
if (file.expires_at && file.expires_at < new Date()) {
|
||||||
await datasource.delete(file.file);
|
await datasource.delete(file.file);
|
||||||
|
@ -242,7 +266,7 @@ async function postFile(file: Image, prisma: PrismaClient) {
|
||||||
|
|
||||||
async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: NextServer, id: string) {
|
async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: NextServer, id: string) {
|
||||||
const data = await datasource.get(id);
|
const data = await datasource.get(id);
|
||||||
if (!data) return nextServer.render404(req, res as ServerResponse);
|
if (!data) return notFound(req, res as ServerResponse, nextServer);
|
||||||
|
|
||||||
const mimetype = await guess(extname(id));
|
const mimetype = await guess(extname(id));
|
||||||
const size = await datasource.size(id);
|
const size = await datasource.size(id);
|
||||||
|
@ -253,7 +277,7 @@ async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: N
|
||||||
data.pipe(res);
|
data.pipe(res);
|
||||||
data.on('error', (e) => {
|
data.on('error', (e) => {
|
||||||
logger.debug(`error while serving raw file ${id}: ${e}`);
|
logger.debug(`error while serving raw file ${id}: ${e}`);
|
||||||
nextServer.render404(req, res as ServerResponse);
|
notFound(req, res as ServerResponse, nextServer);
|
||||||
});
|
});
|
||||||
data.on('end', () => res.end());
|
data.on('end', () => res.end());
|
||||||
}
|
}
|
||||||
|
@ -269,7 +293,7 @@ async function fileDb(
|
||||||
if (Object.keys(exts).includes(ext)) return handle(req, res as ServerResponse);
|
if (Object.keys(exts).includes(ext)) return handle(req, res as ServerResponse);
|
||||||
|
|
||||||
const data = await datasource.get(image.file);
|
const data = await datasource.get(image.file);
|
||||||
if (!data) return nextServer.render404(req, res as ServerResponse);
|
if (!data) return notFound(req, res as ServerResponse, nextServer);
|
||||||
|
|
||||||
const size = await datasource.size(image.file);
|
const size = await datasource.size(image.file);
|
||||||
|
|
||||||
|
@ -279,7 +303,7 @@ async function fileDb(
|
||||||
data.pipe(res);
|
data.pipe(res);
|
||||||
data.on('error', (e) => {
|
data.on('error', (e) => {
|
||||||
logger.debug(`error while serving raw file ${image.file}: ${e}`);
|
logger.debug(`error while serving raw file ${image.file}: ${e}`);
|
||||||
nextServer.render404(req, res as ServerResponse);
|
notFound(req, res as ServerResponse, nextServer);
|
||||||
});
|
});
|
||||||
data.on('end', () => res.end());
|
data.on('end', () => res.end());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue