feat: download certificate for local domain

This commit is contained in:
Nicolas Meienberger 2023-06-06 23:26:46 +02:00 committed by Nicolas Meienberger
parent b6a25566ad
commit 8072cfbce4
8 changed files with 107 additions and 12 deletions

View file

@ -80,6 +80,7 @@ services:
ARCHITECTURE: ${ARCHITECTURE} ARCHITECTURE: ${ARCHITECTURE}
REDIS_HOST: ${REDIS_HOST} REDIS_HOST: ${REDIS_HOST}
DEMO_MODE: ${DEMO_MODE} DEMO_MODE: ${DEMO_MODE}
LOCAL_DOMAIN: ${LOCAL_DOMAIN}
networks: networks:
- tipi_main_network - tipi_main_network
ports: ports:
@ -93,6 +94,7 @@ services:
- ${PWD}/repos:/runtipi/repos:ro - ${PWD}/repos:/runtipi/repos:ro
- ${PWD}/apps:/runtipi/apps - ${PWD}/apps:/runtipi/apps
- ${PWD}/logs:/app/logs - ${PWD}/logs:/app/logs
- ${PWD}/traefik:/runtipi/traefik
- ${STORAGE_PATH}:/app/storage - ${STORAGE_PATH}:/app/storage
labels: labels:
traefik.enable: true traefik.enable: true

View file

@ -76,11 +76,13 @@ services:
ARCHITECTURE: ${ARCHITECTURE} ARCHITECTURE: ${ARCHITECTURE}
REDIS_HOST: ${REDIS_HOST} REDIS_HOST: ${REDIS_HOST}
DEMO_MODE: ${DEMO_MODE} DEMO_MODE: ${DEMO_MODE}
LOCAL_DOMAIN: ${LOCAL_DOMAIN}
volumes: volumes:
- ${PWD}/state:/runtipi/state - ${PWD}/state:/runtipi/state
- ${PWD}/repos:/runtipi/repos:ro - ${PWD}/repos:/runtipi/repos:ro
- ${PWD}/apps:/runtipi/apps - ${PWD}/apps:/runtipi/apps
- ${PWD}/logs:/app/logs - ${PWD}/logs:/app/logs
- ${PWD}/traefik:/runtipi/traefik
- ${PWD}:/app/storage - ${PWD}:/app/storage
labels: labels:
# Main # Main

View file

@ -76,12 +76,14 @@ services:
ARCHITECTURE: ${ARCHITECTURE} ARCHITECTURE: ${ARCHITECTURE}
REDIS_HOST: ${REDIS_HOST} REDIS_HOST: ${REDIS_HOST}
DEMO_MODE: ${DEMO_MODE} DEMO_MODE: ${DEMO_MODE}
LOCAL_DOMAIN: ${LOCAL_DOMAIN}
volumes: volumes:
- ${PWD}/.env:/runtipi/.env - ${PWD}/.env:/runtipi/.env
- ${PWD}/state:/runtipi/state - ${PWD}/state:/runtipi/state
- ${PWD}/repos:/runtipi/repos:ro - ${PWD}/repos:/runtipi/repos:ro
- ${PWD}/apps:/runtipi/apps - ${PWD}/apps:/runtipi/apps
- ${PWD}/logs:/app/logs - ${PWD}/logs:/app/logs
- ${PWD}/traefik:/runtipi/traefik
- ${STORAGE_PATH}:/app/storage - ${STORAGE_PATH}:/app/storage
labels: labels:
# Main # Main

View file

@ -6,7 +6,7 @@ interface IProps {
type?: 'submit' | 'reset' | 'button'; type?: 'submit' | 'reset' | 'button';
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
onClick?: () => void; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode; children: React.ReactNode;
width?: number | null; width?: number | null;
} }

View file

@ -247,6 +247,8 @@
"apps-repo-hint": "URL to the apps repository.", "apps-repo-hint": "URL to the apps repository.",
"storage-path": "Storage path", "storage-path": "Storage path",
"storage-path-hint": "Path to the storage directory. Keep empty for default (runtipi/app-data). Make sure it is an absolute path and that it exists", "storage-path-hint": "Path to the storage directory. Keep empty for default (runtipi/app-data). Make sure it is an absolute path and that it exists",
"local-domain": "Local domain",
"local-domain-hint": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"submit": "Save", "submit": "Save",
"user-settings-title": "User settings", "user-settings-title": "User settings",
"language": "Language", "language": "Language",

View file

@ -2,9 +2,11 @@ import { LanguageSelector } from '@/components/LanguageSelector';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { IconAdjustmentsAlt, IconUser } from '@tabler/icons-react'; import { IconAdjustmentsAlt, IconUser } from '@tabler/icons-react';
import clsx from 'clsx';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Tooltip } from 'react-tooltip';
import validator from 'validator'; import validator from 'validator';
export type SettingsFormValues = { export type SettingsFormValues = {
@ -13,6 +15,7 @@ export type SettingsFormValues = {
appsRepoUrl?: string; appsRepoUrl?: string;
domain?: string; domain?: string;
storagePath?: string; storagePath?: string;
localDomain?: string;
}; };
interface IProps { interface IProps {
@ -29,6 +32,10 @@ export const SettingsForm = (props: IProps) => {
const validateFields = (values: SettingsFormValues) => { const validateFields = (values: SettingsFormValues) => {
const errors: { [K in keyof SettingsFormValues]?: string } = {}; const errors: { [K in keyof SettingsFormValues]?: string } = {};
if (values.localDomain && !validator.isFQDN(values.localDomain)) {
errors.localDomain = t('invalid-domain');
}
if (values.dnsIp && !validator.isIP(values.dnsIp)) { if (values.dnsIp && !validator.isIP(values.dnsIp)) {
errors.dnsIp = t('invalid-ip'); errors.dnsIp = t('invalid-ip');
} }
@ -86,6 +93,11 @@ export const SettingsForm = (props: IProps) => {
} }
}; };
const downloadCertificate = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
window.open('/certificate');
};
return ( return (
<> <>
<div className="d-flex"> <div className="d-flex">
@ -100,23 +112,81 @@ export const SettingsForm = (props: IProps) => {
</div> </div>
<p className="mb-4">{t('subtitle')}</p> <p className="mb-4">{t('subtitle')}</p>
<div className="mb-3"> <div className="mb-3">
<Input {...register('domain')} label={t('domain-name')} error={errors.domain?.message} placeholder="example.com" /> <Input
<span className="text-muted">{t('domain-name-hint')}</span> {...register('domain')}
label={
<>
{t('domain-name')}
<Tooltip anchorSelect=".domain-name-hint">{t('domain-name-hint')}</Tooltip>
<span className={clsx('ms-1 form-help domain-name-hint')}>?</span>
</>
}
error={errors.domain?.message}
placeholder="example.com"
/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<Input {...register('dnsIp')} label={t('dns-ip')} error={errors.dnsIp?.message} placeholder="9.9.9.9" /> <Input {...register('dnsIp')} label={t('dns-ip')} error={errors.dnsIp?.message} placeholder="9.9.9.9" />
</div> </div>
<div className="mb-3"> <div className="mb-3">
<Input {...register('internalIp')} label={t('internal-ip')} error={errors.internalIp?.message} placeholder="192.168.1.100" /> <Input
<span className="text-muted">{t('internal-ip-hint')}</span> {...register('internalIp')}
label={
<>
{t('internal-ip')}
<Tooltip anchorSelect=".internal-ip-hint">{t('internal-ip-hint')}</Tooltip>
<span className={clsx('ms-1 form-help internal-ip-hint')}>?</span>
</>
}
error={errors.internalIp?.message}
placeholder="192.168.1.100"
/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<Input {...register('appsRepoUrl')} label={t('apps-repo')} error={errors.appsRepoUrl?.message} placeholder="https://github.com/meienberger/runtipi-appstore" /> <Input
<span className="text-muted">{t('apps-repo-hint')}</span> {...register('appsRepoUrl')}
label={
<>
{t('apps-repo')}
<Tooltip anchorSelect=".apps-repo-hint">{t('apps-repo-hint')}</Tooltip>
<span className={clsx('ms-1 form-help apps-repo-hint')}>?</span>
</>
}
error={errors.appsRepoUrl?.message}
placeholder="https://github.com/meienberger/runtipi-appstore"
/>
</div> </div>
<div className="mb-3"> <div className="mb-3">
<Input {...register('storagePath')} label={t('storage-path')} error={errors.storagePath?.message} placeholder={t('storage-path')} /> <Input
<span className="text-muted">{t('storage-path-hint')}</span> {...register('storagePath')}
label={
<>
{t('storage-path')}
<Tooltip anchorSelect=".storage-path-hint">{t('storage-path-hint')}</Tooltip>
<span className={clsx('ms-1 form-help storage-path-hint')}>?</span>
</>
}
error={errors.storagePath?.message}
placeholder={t('storage-path')}
/>
</div>
<div className="mb-3">
<Input
{...register('localDomain')}
label={
<>
{t('local-domain')}
<Tooltip anchorSelect=".local-domain-hint">{t('local-domain-hint')}</Tooltip>
<span className={clsx('ms-1 form-help local-domain-hint')}>?</span>
</>
}
error={errors.localDomain?.message}
placeholder="tipi.lan"
/>
<Button className="mt-2" onClick={downloadCertificate}>
Download certificate
</Button>
</div> </div>
<Button loading={loading} type="submit" className="btn-success"> <Button loading={loading} type="submit" className="btn-success">
{t('submit')} {t('submit')}

View file

@ -24,6 +24,7 @@ const configSchema = z.object({
appsRepoId: z.string(), appsRepoId: z.string(),
appsRepoUrl: z.string().url().trim(), appsRepoUrl: z.string().url().trim(),
domain: z.string().trim(), domain: z.string().trim(),
localDomain: z.string().trim(),
storagePath: z storagePath: z
.string() .string()
.trim() .trim()
@ -47,7 +48,7 @@ const configSchema = z.object({
}), }),
}); });
export const settingsSchema = configSchema.partial().pick({ dnsIp: true, internalIp: true, appsRepoUrl: true, domain: true, storagePath: true }); export const settingsSchema = configSchema.partial().pick({ dnsIp: true, internalIp: true, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true });
type TipiSettingsType = z.infer<typeof settingsSchema>; type TipiSettingsType = z.infer<typeof settingsSchema>;
@ -80,6 +81,7 @@ export class TipiConfig {
appsRepoId: conf.APPS_REPO_ID, appsRepoId: conf.APPS_REPO_ID,
appsRepoUrl: conf.APPS_REPO_URL, appsRepoUrl: conf.APPS_REPO_URL,
domain: conf.DOMAIN, domain: conf.DOMAIN,
localDomain: conf.LOCAL_DOMAIN,
dnsIp: conf.DNS_IP || '9.9.9.9', dnsIp: conf.DNS_IP || '9.9.9.9',
status: 'RUNNING', status: 'RUNNING',
storagePath: conf.STORAGE_PATH, storagePath: conf.STORAGE_PATH,

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable global-require */ /* eslint-disable global-require */
import express from 'express'; import express, { Request } from 'express';
import { parse } from 'url'; import { parse } from 'url';
import type { NextServer } from 'next/dist/server/next'; import type { NextServer } from 'next/dist/server/next';
@ -11,6 +11,7 @@ import { runPostgresMigrations } from './run-migration';
import { AppServiceClass } from './services/apps/apps.service'; import { AppServiceClass } from './services/apps/apps.service';
import { db } from './db'; import { db } from './db';
import { sessionMiddleware } from './middlewares/session.middleware'; import { sessionMiddleware } from './middlewares/session.middleware';
import { AuthQueries } from './queries/auth/auth.queries';
let conf = {}; let conf = {};
let nextApp: NextServer; let nextApp: NextServer;
@ -33,12 +34,26 @@ const handle = nextApp.getRequestHandler();
nextApp.prepare().then(async () => { nextApp.prepare().then(async () => {
const app = express(); const app = express();
const authService = new AuthQueries(db);
app.disable('x-powered-by'); app.disable('x-powered-by');
app.use(sessionMiddleware); app.use(sessionMiddleware);
app.use('/static', express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/`)); app.use('/static', express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/`));
app.use('/certificate', async (req, res) => {
const userId = req.session?.userId;
const user = await authService.getUserById(userId);
if (user?.operator) {
res.setHeader('Content-Dispositon', 'attachment; filename=cert.pem');
return res.sendFile(`${getConfig().rootFolder}/traefik/tls/cert.pem`);
}
return res.status(403).send('Forbidden');
});
app.all('*', (req, res) => { app.all('*', (req, res) => {
const parsedUrl = parse(req.url, true); const parsedUrl = parse(req.url, true);
@ -50,7 +65,7 @@ nextApp.prepare().then(async () => {
EventDispatcher.clear(); EventDispatcher.clear();
// Run database migrations // Run database migrations
if (getConfig().NODE_ENV !== 'development') { if (getConfig().NODE_ENV === 'development') {
await runPostgresMigrations(); await runPostgresMigrations();
} }
setConfig('status', 'RUNNING'); setConfig('status', 'RUNNING');