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}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
DEMO_MODE: ${DEMO_MODE}
|
||||
LOCAL_DOMAIN: ${LOCAL_DOMAIN}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
ports:
|
||||
|
@ -93,6 +94,7 @@ services:
|
|||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/runtipi/traefik
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
|
|
|
@ -76,11 +76,13 @@ services:
|
|||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
DEMO_MODE: ${DEMO_MODE}
|
||||
LOCAL_DOMAIN: ${LOCAL_DOMAIN}
|
||||
volumes:
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/runtipi/traefik
|
||||
- ${PWD}:/app/storage
|
||||
labels:
|
||||
# Main
|
||||
|
|
|
@ -76,12 +76,14 @@ services:
|
|||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
DEMO_MODE: ${DEMO_MODE}
|
||||
LOCAL_DOMAIN: ${LOCAL_DOMAIN}
|
||||
volumes:
|
||||
- ${PWD}/.env:/runtipi/.env
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/runtipi/traefik
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
# Main
|
||||
|
|
|
@ -6,7 +6,7 @@ interface IProps {
|
|||
type?: 'submit' | 'reset' | 'button';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
children: React.ReactNode;
|
||||
width?: number | null;
|
||||
}
|
||||
|
|
|
@ -247,6 +247,8 @@
|
|||
"apps-repo-hint": "URL to the apps repository.",
|
||||
"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",
|
||||
"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",
|
||||
"user-settings-title": "User settings",
|
||||
"language": "Language",
|
||||
|
|
|
@ -2,9 +2,11 @@ import { LanguageSelector } from '@/components/LanguageSelector';
|
|||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { IconAdjustmentsAlt, IconUser } from '@tabler/icons-react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import validator from 'validator';
|
||||
|
||||
export type SettingsFormValues = {
|
||||
|
@ -13,6 +15,7 @@ export type SettingsFormValues = {
|
|||
appsRepoUrl?: string;
|
||||
domain?: string;
|
||||
storagePath?: string;
|
||||
localDomain?: string;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
|
@ -29,6 +32,10 @@ export const SettingsForm = (props: IProps) => {
|
|||
const validateFields = (values: SettingsFormValues) => {
|
||||
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)) {
|
||||
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 (
|
||||
<>
|
||||
<div className="d-flex">
|
||||
|
@ -100,23 +112,81 @@ export const SettingsForm = (props: IProps) => {
|
|||
</div>
|
||||
<p className="mb-4">{t('subtitle')}</p>
|
||||
<div className="mb-3">
|
||||
<Input {...register('domain')} label={t('domain-name')} error={errors.domain?.message} placeholder="example.com" />
|
||||
<span className="text-muted">{t('domain-name-hint')}</span>
|
||||
<Input
|
||||
{...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 className="mb-3">
|
||||
<Input {...register('dnsIp')} label={t('dns-ip')} error={errors.dnsIp?.message} placeholder="9.9.9.9" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input {...register('internalIp')} label={t('internal-ip')} error={errors.internalIp?.message} placeholder="192.168.1.100" />
|
||||
<span className="text-muted">{t('internal-ip-hint')}</span>
|
||||
<Input
|
||||
{...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 className="mb-3">
|
||||
<Input {...register('appsRepoUrl')} label={t('apps-repo')} error={errors.appsRepoUrl?.message} placeholder="https://github.com/meienberger/runtipi-appstore" />
|
||||
<span className="text-muted">{t('apps-repo-hint')}</span>
|
||||
<Input
|
||||
{...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 className="mb-3">
|
||||
<Input {...register('storagePath')} label={t('storage-path')} error={errors.storagePath?.message} placeholder={t('storage-path')} />
|
||||
<span className="text-muted">{t('storage-path-hint')}</span>
|
||||
<Input
|
||||
{...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>
|
||||
<Button loading={loading} type="submit" className="btn-success">
|
||||
{t('submit')}
|
||||
|
|
|
@ -24,6 +24,7 @@ const configSchema = z.object({
|
|||
appsRepoId: z.string(),
|
||||
appsRepoUrl: z.string().url().trim(),
|
||||
domain: z.string().trim(),
|
||||
localDomain: z.string().trim(),
|
||||
storagePath: z
|
||||
.string()
|
||||
.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>;
|
||||
|
||||
|
@ -80,6 +81,7 @@ export class TipiConfig {
|
|||
appsRepoId: conf.APPS_REPO_ID,
|
||||
appsRepoUrl: conf.APPS_REPO_URL,
|
||||
domain: conf.DOMAIN,
|
||||
localDomain: conf.LOCAL_DOMAIN,
|
||||
dnsIp: conf.DNS_IP || '9.9.9.9',
|
||||
status: 'RUNNING',
|
||||
storagePath: conf.STORAGE_PATH,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable global-require */
|
||||
import express from 'express';
|
||||
import express, { Request } from 'express';
|
||||
import { parse } from 'url';
|
||||
|
||||
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 { db } from './db';
|
||||
import { sessionMiddleware } from './middlewares/session.middleware';
|
||||
import { AuthQueries } from './queries/auth/auth.queries';
|
||||
|
||||
let conf = {};
|
||||
let nextApp: NextServer;
|
||||
|
@ -33,12 +34,26 @@ const handle = nextApp.getRequestHandler();
|
|||
|
||||
nextApp.prepare().then(async () => {
|
||||
const app = express();
|
||||
const authService = new AuthQueries(db);
|
||||
|
||||
app.disable('x-powered-by');
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
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) => {
|
||||
const parsedUrl = parse(req.url, true);
|
||||
|
||||
|
@ -50,7 +65,7 @@ nextApp.prepare().then(async () => {
|
|||
EventDispatcher.clear();
|
||||
|
||||
// Run database migrations
|
||||
if (getConfig().NODE_ENV !== 'development') {
|
||||
if (getConfig().NODE_ENV === 'development') {
|
||||
await runPostgresMigrations();
|
||||
}
|
||||
setConfig('status', 'RUNNING');
|
||||
|
|
Loading…
Reference in a new issue