feat: download certificate for local domain
This commit is contained in:
parent
b6a25566ad
commit
8072cfbce4
8 changed files with 107 additions and 12 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Reference in a new issue