Merge pull request #11 from meienberger/develop

Release 0.0.1
This commit is contained in:
Nicolas Meienberger 2022-05-03 21:02:39 +00:00 committed by GitHub
commit 2bd5d1e28e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
170 changed files with 4771 additions and 14984 deletions

10
.gitignore vendored
View file

@ -13,4 +13,12 @@ state/*
tipi.config.json
# Commit empty directories
!nignx/.gitkeep
!nignx/.gitkeep
media/data/movies/*
media/data/tv/*
!media/data/movies/.gitkeep
!media/data/tv/.gitkeep
media/torrents/*
!media/torrents/.gitkeep

View file

@ -3,3 +3,5 @@
![RunsOn](https://img.shields.io/badge/Ubuntu-Supported-green?logo=ubuntu&style=flat-square)
![RunsOn](https://img.shields.io/badge/Arch-Not%20Supported-red?logo=archlinux&style=flat-square)
![RunsOn](https://img.shields.io/badge/Fedora-Not%20Supported-red?logo=fedora&style=flat-square)
![Preview](https://raw.githubusercontent.com/meienberger/runtipi/develop/screenshots/1.png)

View file

@ -5,9 +5,8 @@ packages:
- iptables
- coreutils
- git
- base-devel
- docker
- avahi
- avahi-daemon
- nodejs
- npm
@ -15,4 +14,4 @@ username: nicolas
### ZSH Settings
zsh_theme: "powerlevel10k/powerlevel10k"
ohmyzsh_git_url: https://github.com/robbyrussell/oh-my-zsh
ohmyzsh_git_url: https://github.com/robbyrussell/oh-my-zsh

View file

@ -7,10 +7,5 @@
- import_tasks: ./tasks/common/zsh.yml
- import_tasks: ./tasks/common/docker.yml
- import_tasks: ./tasks/network/avahi.yml
# - import_tasks: tasks/zsh.yml
# - import_tasks: tasks/nginx.yml
# - import_tasks: tasks/pi-hole.yml
# - import_tasks: tasks/pi-vpn.yml
# - import_tasks: tasks/nextcloud.yml
# - name: Reboot machine
# reboot:

6
ansible/stop.yml Normal file
View file

@ -0,0 +1,6 @@
---
- hosts: tipi
become: yes
tasks:
- import_tasks: ./tasks/common/teardown.yml

View file

@ -1,8 +1,33 @@
- name: Install docker
package:
name: docker
name:
- docker
- ca-certificates
- curl
- gnupg
- lsb-release
state: latest
- name: Add docker gpg key
shell: curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- name: Check lsb_release -cs
shell: lsb_release -cs
register: lsb_release
- name: Add deb for bookworm release
shell: echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
when: lsb_release.stdout == "bookworm"
- name: Add deb for non-bookworm
shell: echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
when: lsb_release.stdout != "bookworm"
- name: Update packages
apt:
update_cache: yes
upgrade: yes
- name: Install essential packages
package:
name:

View file

@ -1,22 +1,18 @@
- name: Change machine hostname to tipi.local
shell: hostnamectl set-hostname tipi.local
- name: Update packages
become: yes
pacman:
update_cache: yes
upgrade: yes
- name: Add user to root group
user:
name: "{{ username }}"
group: root
# - name: Update packages
# apt:
# update_cache: yes
# upgrade: yes
- name: Install essential packages
package:
name: "{{ packages }}"
state: latest
- name: Add user to root group
user:
name: "{{ username }}"
group: root
- name: Disable SSH password auth
lineinfile:
dest: /etc/ssh/sshd_config

View file

@ -1,6 +1,6 @@
- name: Install "pm2" package globally.
community.general.npm:
name: yarn
name: pm2
global: yes
- name: Run pm2 first time
@ -29,15 +29,15 @@
- name: Check if app is already running
become_user: "{{ username }}"
shell: pm2 list
shell: pm2 status system-api
register: pm2_result
- name: Start app
become_user: "{{ username }}"
shell: cd {{ playbook_dir }}/../system-api && pm2 start npm --name "system-api" -- start
when: pm2_result.stdout.find("system-api") == -1
when: pm2_result.stdout.find("online") == -1
- name: Reload app
become_user: "{{ username }}"
shell: pm2 reload system-api
when: pm2_result.stdout.find("system-api") != -1
when: pm2_result.stdout.find("online") != -1

View file

@ -0,0 +1,9 @@
- name: Check if app is already running
become_user: "{{ username }}"
shell: pm2 list
register: pm2_result
- name: Stop app
become_user: "{{ username }}"
shell: pm2 stop "system-api"
when: pm2_result.stdout.find("system-api") != -1

View file

@ -7,10 +7,10 @@
<port>80</port>
</service>
</service-group>
<service-group>
<!-- <service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_http._tcp</type>
<port>443</port>
</service>
</service-group>
</service-group> -->

45
apps/anonaddy/config.json Normal file
View file

@ -0,0 +1,45 @@
{
"name": "Anonaddy",
"port": 8084,
"id": "anonaddy",
"description": "",
"short_desc": "Anonymous email forwarding",
"author": "",
"source": "https://github.com/anonaddy/anonaddy",
"image": "https://avatars.githubusercontent.com/u/51450862?s=200&v=4",
"requirements": {
"ports": [25]
},
"form_fields": {
"username": {
"type": "text",
"label": "Username",
"required": true,
"env_variable": "ANONADDY_USERNAME"
},
"key": {
"type": "text",
"label": "App key",
"hint": "Application key for encrypter service. Generate one with : echo \"base64:$(openssl rand -base64 32)\"",
"required": true,
"env_variable": "ANONADDY_KEY"
},
"domain": {
"type": "fqdn",
"label": "Your email domain (eg. example.com)",
"max": 50,
"min": 3,
"required": true,
"env_variable": "ANONADDY_DOMAIN"
},
"secret": {
"type": "text",
"label": "App secret",
"hint": "Long random string used when hashing data for the anonymous replies",
"max": 50,
"min": 3,
"required": true,
"env_variable": "ANONADDY_SECRET"
}
}
}

View file

@ -32,29 +32,32 @@ services:
container_name: anonaddy
ports:
- 25:25
- ${APP_ANONADDY_PORT}:8000
- ${APP_PORT}:8000
depends_on:
- db
- redis
volumes:
- "${APP_DATA_DIR}/data:/data"
environment:
TZ: ${TZ}
DB_HOST: db-anonaddy
DB_PASSWORD: anonaddy
REDIS_HOST: redis-anonaddy
APP_KEY: ${APP_ANONADDY_KEY}
ANONADDY_DOMAIN: ${APP_ANONADDY_DOMAIN}
ANONADDY_SECRET: ${APP_ANONADDY_SECRET}
APP_KEY: ${ANONADDY_KEY}
ANONADDY_DOMAIN: ${ANONADDY_DOMAIN}
ANONADDY_SECRET: ${ANONADDY_SECRET}
ANONADDY_ADMIN_USERNAME: ${ANONADDY_USERNAME}
POSTFIX_DEBUG: true
restart: unless-stopped
networks:
- tipi_main_network
labels:
traefik.enable: true
traefik.http.routers.anonaddy.rule: Host(`anonaddy.tipi.home`)
traefik.http.routers.anonaddy.tls: true
traefik.http.routers.anonaddy.entrypoints: websecure
traefik.http.routers.anonaddy.service: anonaddy
traefik.http.services.anonaddy.loadbalancer.server.port: 8000
# labels:
# traefik.enable: true
# traefik.http.routers.anonaddy.rule: Host(`anonaddy.tipi.home`)
# traefik.http.routers.anonaddy.tls: true
# traefik.http.routers.anonaddy.entrypoints: websecure
# traefik.http.routers.anonaddy.service: anonaddy
# traefik.http.services.anonaddy.loadbalancer.server.port: 8000
# labels:
# traefik.enable: true
# traefik.http.routers.anonaddy.rule: PathPrefix(`/anonaddy`)

View file

@ -2,5 +2,4 @@ version: "3.7"
networks:
tipi_main_network:
external:
name: runtipi_tipi_main_network

View file

@ -0,0 +1,12 @@
{
"name": "File Browser",
"port": 8096,
"id": "filebrowser",
"description": "Reliable and Performant File Management Desktop Sync and File Sharing",
"short_desc": "Access your homeserver files from your browser",
"author": "",
"website": "https://filebrowser.org/",
"source": "https://github.com/filebrowser/filebrowser",
"image": "https://avatars.githubusercontent.com/u/35781395?s=200&v=4",
"form_fields": {}
}

View file

@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}

View file

@ -0,0 +1,15 @@
services:
filebrowser:
container_name: filebrowser
image: filebrowser/filebrowser:s6
ports:
- ${APP_PORT}:80
environment:
- PUID=1000
- PGID=1000
volumes:
- ${ROOT_FOLDER}:/srv
- ${APP_DATA_DIR}/data/filebrowser.db:/database/filebrowser.db
- ${APP_DATA_DIR}/data/settings.json:/config/settings.json
networks:
- tipi_main_network

11
apps/filerun/config.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "FileRun",
"port": 8087,
"id": "filerun",
"description": "Reliable and Performant File Management Desktop Sync and File Sharing",
"short_desc": "Access your homeserver files from your browser",
"author": "FileRun, LDA - Portugal",
"source": "https://www.filerun.com/",
"image": "https://avatars.githubusercontent.com/u/6422152?v=4",
"form_fields": {}
}

View file

@ -0,0 +1,38 @@
services:
filerun-db:
container_name: filerun-db
user: 1000:1000
image: mariadb:10.1
environment:
MYSQL_ROOT_PASSWORD: tipi
MYSQL_USER: tipi
MYSQL_PASSWORD: tipi
MYSQL_DATABASE: tipi
volumes:
- ${APP_DATA_DIR}/data/db:/var/lib/mysql
networks:
- tipi_main_network
filerun:
container_name: filerun
image: filerun/filerun:arm64v8
environment:
FR_DB_HOST: filerun-db
FR_DB_PORT: 3306
FR_DB_NAME: tipi
FR_DB_USER: tipi
FR_DB_PASS: tipi
APACHE_RUN_USER: 1000
APACHE_RUN_GROUP: 1000
APACHE_RUN_USER_ID: 33
APACHE_RUN_GROUP_ID: 33
depends_on:
- db
links:
- db:db
ports:
- ${APP_PORT}:80
volumes:
- ${ROOT_FOLDER}/app-data/medias:/user-files
networks:
- tipi_main_network

11
apps/freshrss/config.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "FreshRSS",
"port": 8086,
"id": "freshrss",
"description": "FreshRSS is a self-hosted RSS feed aggregator like Leed or Kriss Feed.\nIt is lightweight, easy to work with, powerful, and customizable.\n\nIt is a multi-user application with an anonymous reading mode. It supports custom tags. There is an API for (mobile) clients, and a Command-Line Interface.\n\nThanks to the WebSub standard (formerly PubSubHubbub), FreshRSS is able to receive instant push notifications from compatible sources, such as Mastodon, Friendica, WordPress, Blogger, FeedBurner, etc.\n\nFreshRSS natively supports basic Web scraping, based on XPath, for Web sites not providing any RSS / Atom feed.\n\nFinally, it supports extensions for further tuning.",
"short_desc": "A free, self-hostable aggregator… ",
"author": "https://freshrss.org/",
"source": "https://github.com/FreshRSS/FreshRSS",
"image": "https://avatars.githubusercontent.com/u/9414285?s=200&v=4",
"form_fields": {}
}

View file

@ -6,19 +6,19 @@ services:
image: freshrss/freshrss:arm
restart: unless-stopped
ports:
- "${APP_FRESHRSS_PORT}:80"
- ${APP_PORT}:80
volumes:
- ${APP_DATA_DIR}/data/:/var/www/FreshRSS/data
- ${APP_DATA_DIR}/extensions/:/var/www/FreshRSS/extensions
- ${APP_DATA_DIR}/data/freshrss:/var/www/FreshRSS/data
- ${APP_DATA_DIR}/data/extensions/:/var/www/FreshRSS/extensions
environment:
CRON_MIN: '*/20'
TZ: $TZ
networks:
- tipi_main_network
labels:
traefik.enable: true
traefik.http.routers.freshrss.rule: Host(`freshrss.tipi.home`)
traefik.http.routers.freshrss.service: freshrss
traefik.http.routers.freshrss.tls: true
traefik.http.routers.freshrss.entrypoints: websecure
traefik.http.services.freshrss.loadbalancer.server.port: 80
# labels:
# traefik.enable: true
# traefik.http.routers.freshrss.rule: Host(`freshrss.tipi.home`)
# traefik.http.routers.freshrss.service: freshrss
# traefik.http.routers.freshrss.tls: true
# traefik.http.routers.freshrss.entrypoints: websecure
# traefik.http.services.freshrss.loadbalancer.server.port: 80

View file

@ -0,0 +1,11 @@
{
"name": "Invidious",
"port": 8095,
"id": "invidious",
"description": "",
"short_desc": "",
"author": "",
"source": "https://github.com/iv-org/invidious",
"image": "https://raw.githubusercontent.com/iv-org/invidious/master/assets/invidious-colored-vector.svg",
"form_fields": {}
}

View file

@ -0,0 +1,45 @@
version: "3"
services:
invidious:
user: 1000:1000
container_name: invidious
image: quay.io/invidious/invidious:latest-arm64
# image: quay.io/invidious/invidious:latest-arm64 # ARM64/AArch64 devices
restart: unless-stopped
ports:
- "${APP_PORT}:3000"
environment:
# Please read the following file for a comprehensive list of all available
# configuration options and their associated syntax:
# https://github.com/iv-org/invidious/blob/master/config/config.example.yml
INVIDIOUS_CONFIG: |
db:
dbname: invidious
user: tipi
password: tipi
host: invidious-db
port: 5432
check_tables: true
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/comments/jNQXAC9IVRw || exit 1
interval: 30s
timeout: 5s
retries: 2
depends_on:
- invidious-db
invidious-db:
user: 1000:1000
container_name: invidious-db
image: docker.io/library/postgres:14
restart: unless-stopped
volumes:
- ${APP_DATA_DIR}/data/postgres:/var/lib/postgresql/data
- ${APP_DATA_DIR}/data/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
environment:
POSTGRES_DB: invidious
POSTGRES_USER: tipi
POSTGRES_PASSWORD: tipi
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]

13
apps/jackett/config.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "Jackett",
"port": 8097,
"id": "jackett",
"description": "Jackett works as a proxy server: it translates queries from apps (Sonarr, Radarr, SickRage, CouchPotato, Mylar3, Lidarr, DuckieTV, qBittorrent, Nefarious etc.) into tracker-site-specific http queries, parses the html or json response, and then sends results back to the requesting software. This allows for getting recent uploads (like RSS) and performing searches.",
"short_desc": "API Support for your favorite torrent trackers ",
"author": "",
"source": "https://github.com/Jackett/Jackett",
"image": "https://avatars.githubusercontent.com/u/15383019?s=200&v=4",
"form_fields": {
}
}

View file

@ -0,0 +1,20 @@
version: "3.7"
services:
jackett:
image: lscr.io/linuxserver/jackett
container_name: jackett
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
- AUTO_UPDATE=true
dns:
- ${DNS_IP}
volumes:
- ${APP_DATA_DIR}/data:/config
- ${ROOT_FOLDER}/media/torrents:/downloads
ports:
- ${APP_PORT}:9117
restart: unless-stopped
networks:
- tipi_main_network

11
apps/jellyfin/config.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "Jellyfin",
"port": 8091,
"id": "jellyfin",
"description": "",
"short_desc": "",
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/45698031?s=200&v=4",
"form_fields": {}
}

View file

@ -0,0 +1,18 @@
version: "3.7"
services:
jellyfin:
image: lscr.io/linuxserver/jellyfin
container_name: jellyfin
volumes:
- ${APP_DATA_DIR}/data/config:/config
- ${ROOT_FOLDER}/media/data:/data/media
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
restart: "unless-stopped"
ports:
- ${APP_PORT}:8096
networks:
- tipi_main_network

13
apps/joplin/README.md Normal file
View file

@ -0,0 +1,13 @@
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in Markdown format.
Notes exported from Evernote can be imported into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
The notes can be securely synchronised using end-to-end encryption with various cloud services including Nextcloud, Dropbox, OneDrive and Joplin Cloud.
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
The application is available for Windows, Linux, macOS, Android and iOS. A Web Clipper, to save web pages and screenshots from your browser, is also available for Firefox and Chrome.
## Credentials
Username: admin@localhost
Password: admin

12
apps/joplin/config.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "Joplin Server",
"port": 8099,
"id": "joplin",
"description": "",
"short_desc": "Note taking and to-do application with synchronisation",
"author": "https://github.com/laurent22",
"source": "https://github.com/laurent22/joplin",
"website": "https://joplinapp.org",
"image": "https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png",
"form_fields": {}
}

View file

@ -0,0 +1,38 @@
version: "3.7"
services:
db-joplin:
container_name: db-joplin
image: postgres:14.2
volumes:
- ${APP_DATA_DIR}/data/postgres:/var/lib/postgresql/data
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=tipi
- POSTGRES_USER=tipi
- POSTGRES_DB=joplin
networks:
- tipi_main_network
joplin:
container_name: joplin
image: florider89/joplin-server:2.7.4
restart: unless-stopped
depends_on:
- db-joplin
ports:
- ${APP_PORT}:22300
dns:
- ${DNS_IP}
environment:
- APP_PORT=22300
- APP_BASE_URL=http://${INTERNAL_IP}:${APP_PORT}
- DB_CLIENT=pg
- POSTGRES_PASSWORD=tipi
- POSTGRES_USER=tipi
- POSTGRES_DATABASE=joplin
- POSTGRES_PORT=5432
- POSTGRES_HOST=db-joplin
- MAX_TIME_DRIFT=0
networks:
- tipi_main_network

12
apps/n8n/config.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "n8n",
"port": 8094,
"id": "n8n",
"description": "n8n is an extendable workflow automation tool. With a fair-code distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.",
"short_desc": "Workflow Automation Tool. Alternative to Zapier",
"author": "n8n.io",
"source": "https://github.com/n8n-io/n8n",
"website": "https://n8n.io/",
"image": "https://avatars.githubusercontent.com/u/45487711?s=200&v=4",
"form_fields": {}
}

View file

@ -0,0 +1,36 @@
version: "3.7"
services:
db-n8n:
container_name: db-n8n
image: postgres:14.2
restart: on-failure
volumes:
- ${APP_DATA_DIR}/data/db:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=tipi
- POSTGRES_USER=tipi
- POSTGRES_DB=n8n
networks:
- tipi_main_network
n8n:
container_name: n8n
image: n8nio/n8n:0.174.0
restart: unless-stopped
ports:
- ${APP_PORT}:5678
volumes:
- ${APP_DATA_DIR}/data/n8n:/home/node/.n8n
command: /bin/sh -c "sleep 5; n8n start"
environment:
- DB-TYPE=postgresdb
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_HOST=db-n8n
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_USER=tipi
- DB_POSTGRESDB_PASSWORD=tipi
depends_on:
- db-n8n
networks:
- tipi_main_network

View file

@ -1,6 +1,12 @@
{
"name": "Nextcloud",
"port": 8083,
"id": "nextcloud",
"description": "Nextcloud is a self-hosted, open source, and fully-featured cloud storage solution for your personal files, office documents, and photos.",
"short_desc": "Productivity platform that keeps you in control",
"author": "Nextcloud GmbH",
"source": "https://github.com/nextcloud/server",
"image": "https://avatars.githubusercontent.com/u/19211038?s=200&v=4",
"form_fields": {
"username": {
"type": "text",
@ -8,7 +14,7 @@
"max": 50,
"min": 3,
"required": true,
"env_variable": "NEXTCLOUD_USERNAME"
"env_variable": "NEXTCLOUD_ADMIN_USER"
},
"password": {
"type": "password",
@ -16,7 +22,7 @@
"max": 50,
"min": 3,
"required": true,
"env_variable": "NEXTCLOUD_PASSWORD"
"env_variable": "NEXTCLOUD_ADMIN_PASSWORD"
}
}
}

View file

@ -3,24 +3,21 @@ version: "3.7"
services:
db-nextcloud:
container_name: db-nextcloud
# user: '1000:1000'
image: mariadb:10.5.12
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
image: postgres:14.2
restart: on-failure
volumes:
- ${APP_DATA_DIR}/data/db:/var/lib/mysql
- ${APP_DATA_DIR}/data/db:/var/lib/postgresql/data
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- POSTGRES_PASSWORD=tipi
- POSTGRES_USER=tipi
- POSTGRES_DB=nextcloud
networks:
- tipi_main_network
redis-nextcloud:
container_name: redis-nextcloud
# user: '1000:1000'
image: redis:6.2.2-buster
user: "1000:1000"
image: redis:6.2.6
restart: on-failure
volumes:
- "${APP_DATA_DIR}/data/redis:/data"
@ -28,7 +25,7 @@ services:
- tipi_main_network
cron:
image: nextcloud:22.0.0-apache
image: nextcloud:23.0.3-apache
restart: on-failure
volumes:
- ${APP_DATA_DIR}/data/nextcloud:/var/www/html
@ -40,23 +37,22 @@ services:
- tipi_main_network
nextcloud:
user: root
container_name: nextcloud
image: nextcloud:22.1.1-apache
image: nextcloud:23.0.3-apache
restart: unless-stopped
ports:
- ${APP_NEXTCLOUD_PORT}:80
- ${APP_PORT}:80
volumes:
- ${APP_DATA_DIR}/data/nextcloud:/var/www/html
environment:
- MYSQL_HOST=db-nextcloud
- POSTGRES_HOST=db-nextcloud
- REDIS_HOST=redis-nextcloud
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- POSTGRES_PASSWORD=tipi
- POSTGRES_USER=tipi
- POSTGRES_DB=nextcloud
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=tipi.local
- NEXTCLOUD_TRUSTED_DOMAINS=${DEVICE_IP}:${APP_PORT}
depends_on:
- db-nextcloud
- redis-nextcloud

View file

@ -1,24 +0,0 @@
## DNS Over TLS, Simple ENCRYPTED recursive caching DNS, TCP port 853
## unbound.conf, original at https://calomel.org/unbound_dns.html
# tweaks by bartonbytes.com
server:
access-control: 127.0.0.0/8 allow
cache-max-ttl: 14400
cache-min-ttl: 600
do-tcp: yes
hide-identity: yes
hide-version: yes
interface: 127.0.0.1
minimal-responses: yes
prefetch: yes
qname-minimisation: yes
rrset-roundrobin: yes
ssl-upstream: yes
use-caps-for-id: yes
verbosity: 1
port: 5533
#
forward-zone:
name: "."
forward-addr: 194.242.2.3@853 # Mullvad primary
forward-addr: 193.19.108.3@853 # Mullvad secondary

View file

@ -1,34 +0,0 @@
version: "3.7"
services:
unbound:
user: '1000:1000'
image: "klutchell/unbound:latest"
volumes:
- ${APP_DATA_DIR}/data/unbound:/etc/unbound
networks:
- tipi_main_network
pihole:
image: pihole/pihole
restart: on-failure
ports:
- 53:53
- 53:53/udp
- ${APP_PI_HOLE_PORT}:80
volumes:
- ${APP_DATA_DIR}/data/pihole:/etc/pihole/
- ${APP_DATA_DIR}/data/dnsmasq:/etc/dnsmasq.d/
environment:
- VIRTUAL_HOST="pihole.${DOMAIN}"
- WEBPASSWORD=${APP_PASSWORD}
- PIHOLE_DNS=unbound
depends_on:
- unbound
networks:
- tipi_main_network
labels:
traefik.enable: true
traefik.http.routers.traefik.rule: Host(`pihole.${DOMAIN}`)
traefik.http.services.traefik.loadbalancer.server.port: $APP_PI_HOLE_PORT

23
apps/pihole/config.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "PiHole",
"port": 8081,
"requirements": {
"ports": [53]
},
"id": "pihole",
"description": "",
"short_desc": "",
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/16827203?s=200&v=4",
"form_fields": {
"password": {
"type": "password",
"label": "Password",
"max": 50,
"min": 3,
"required": true,
"env_variable": "APP_PASSWORD"
}
}
}

View file

@ -0,0 +1,6 @@
# A Record
#local-data: "somecomputer.local. A 192.168.1.1"
# PTR Record
#local-data-ptr: "192.168.1.1 somecomputer.local."

View file

@ -0,0 +1,92 @@
; This file holds the information on root name servers needed to
; initialize cache of Internet domain name servers
; (e.g. reference this file in the "cache . <file>"
; configuration file of BIND domain name servers).
;
; This file is made available by InterNIC
; under anonymous FTP as
; file /domain/named.cache
; on server FTP.INTERNIC.NET
; -OR- RS.INTERNIC.NET
;
; last update: December 07, 2021
; related version of root zone: 2021120701
;
; FORMERLY NS.INTERNIC.NET
;
. 3600000 NS A.ROOT-SERVERS.NET.
A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4
A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30
;
; FORMERLY NS1.ISI.EDU
;
. 3600000 NS B.ROOT-SERVERS.NET.
B.ROOT-SERVERS.NET. 3600000 A 199.9.14.201
B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:200::b
;
; FORMERLY C.PSI.NET
;
. 3600000 NS C.ROOT-SERVERS.NET.
C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12
C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c
;
; FORMERLY TERP.UMD.EDU
;
. 3600000 NS D.ROOT-SERVERS.NET.
D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13
D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d
;
; FORMERLY NS.NASA.GOV
;
. 3600000 NS E.ROOT-SERVERS.NET.
E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10
E.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:a8::e
;
; FORMERLY NS.ISC.ORG
;
. 3600000 NS F.ROOT-SERVERS.NET.
F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241
F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f
;
; FORMERLY NS.NIC.DDN.MIL
;
. 3600000 NS G.ROOT-SERVERS.NET.
G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4
G.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:12::d0d
;
; FORMERLY AOS.ARL.ARMY.MIL
;
. 3600000 NS H.ROOT-SERVERS.NET.
H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53
H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53
;
; FORMERLY NIC.NORDU.NET
;
. 3600000 NS I.ROOT-SERVERS.NET.
I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17
I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53
;
; OPERATED BY VERISIGN, INC.
;
. 3600000 NS J.ROOT-SERVERS.NET.
J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30
J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30
;
; OPERATED BY RIPE NCC
;
. 3600000 NS K.ROOT-SERVERS.NET.
K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129
K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1
;
; OPERATED BY ICANN
;
. 3600000 NS L.ROOT-SERVERS.NET.
L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42
L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42
;
; OPERATED BY WIDE
;
. 3600000 NS M.ROOT-SERVERS.NET.
M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33
M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35
; End of file

View file

@ -0,0 +1,9 @@
; autotrust trust anchor file
;;id: . 1
;;last_queried: 1650921300 ;;Mon Apr 25 21:15:00 2022
;;last_success: 1650921300 ;;Mon Apr 25 21:15:00 2022
;;next_probe_time: 1650962281 ;;Tue Apr 26 08:38:01 2022
;;query_failed: 0
;;query_interval: 43200
;;retry_time: 8640
. 86400 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU= ;{id = 20326 (ksk), size = 2048b} ;;state=2 [ VALID ] ;;count=0 ;;lastchange=1650921210 ;;Mon Apr 25 21:13:30 2022

View file

@ -0,0 +1,136 @@
# https://linux.die.net/man/5/unbound.conf
# https://docs.pi-hole.net/guides/unbound/
server:
# Enable or disable whether the unbound server forks into the background
# as a daemon. Default is yes.
do-daemonize: no
# If given, after binding the port the user privileges are dropped.
# Default is "unbound". If you give username: "" no user change is performed.
username: ""
# No need to chroot as this container has been stripped of all other binaries.
chroot: ""
# If "" is given, logging goes to stderr, or nowhere once daemonized.
logfile: ""
# The process id is written to the file. Not required since we are running
# in a container with one process.
pidfile: ""
# The verbosity number, level 0 means no verbosity, only errors.
# Level 1 gives operational information.
# Level 2 gives detailed operational information.
# Level 3 gives query level information, output per query.
# Level 4 gives algorithm level information.
# Level 5 logs client identification for cache misses.
# Default is level 1. The verbosity can also be increased from the commandline.
verbosity: 1
# Listen on all ipv4 interfaces, answer queries from the local subnet.
interface: 0.0.0.0
# The port number, default 53, on which the server responds to queries.
port: 53
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
# You want to leave this to no unless you have *native* IPv6. With 6to4 and
# Terredo tunnels your web browser should favor IPv4 for the same reasons
prefer-ip6: no
# Trust glue only if it is within the server's authority
harden-glue: yes
# Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
harden-dnssec-stripped: yes
# Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
# see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
use-caps-for-id: no
# Reduce EDNS reassembly buffer size (see also https://docs.pi-hole.net/guides/dns/unbound/ )
# IP fragmentation is unreliable on the Internet today, and can cause
# transmission failures when large DNS messages are sent via UDP. Even
# when fragmentation does work, it may not be secure; it is theoretically
# possible to spoof parts of a fragmented DNS message, without easy
# detection at the receiving end. Recently, there was an excellent study
# >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
# by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
# in collaboration with NLnet Labs explored DNS using real world data from the
# the RIPE Atlas probes and the researchers suggested different values for
# IPv4 and IPv6 and in different scenarios. They advise that servers should
# be configured to limit DNS messages sent over UDP to a size that will not
# trigger fragmentation on typical network links. DNS servers can switch
# from UDP to TCP when a DNS response is too big to fit in this limited
# buffer size. This value has also been suggested in DNS Flag Day 2020.
edns-buffer-size: 1232
# Perform prefetching of close to expired message cache entries
# This only applies to domains that have been frequently queried
prefetch: yes
# One thread should be sufficient, can be increased on beefy machines.
# In reality for most users running on small networks or on a single machine,
# it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
num-threads: 1
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
# (requires CAP_NET_ADMIN or privileged)
# so-rcvbuf: 1m
# The netblock is given as an IP4 or IP6 address with /size appended for a
# classless network block. The action can be deny, refuse, allow or allow_snoop.
access-control: 127.0.0.1/32 allow
access-control: 192.168.0.0/16 allow
access-control: 172.16.0.0/12 allow
access-control: 10.0.0.0/8 allow
access-control: 100.64.0.0/10 allow
access-control: 10.21.21.0/24 allow
# Ensure privacy of local IP ranges
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
# Read the root hints from this file. Default is nothing, using built in
# hints for the IN class. The file has the format of zone files, with root
# nameserver names and addresses only. The default may become outdated,
# when servers change, therefore it is good practice to use a root-hints
# file. get one from https://www.internic.net/domain/named.root
root-hints: /etc/unbound/root.hints
# File with trust anchor for one zone, which is tracked with RFC5011 probes.
# The probes are several times per month, thus the machine must be online frequently.
# The initial file can be one with contents as described in trust-anchor-file.
# The file is written to when the anchor is updated, so the unbound user must
# have write permission.
auto-trust-anchor-file: /etc/unbound/root.key
# Number of ports to open. This number of file descriptors can be opened per thread.
# Must be at least 1. Default depends on compile options. Larger numbers need extra
# resources from the operating system. For performance a very large value is best,
# use libevent to make this possible.
outgoing-range: 8192
# The number of queries that every thread will service simultaneously. If more queries
# arrive that need servicing, and no queries can be jostled out (see jostle-timeout),
# then the queries are dropped. This forces the client to resend after a timeout;
# allowing the server time to work on the existing queries. Default depends on
# compile options, 512 or 1024.
num-queries-per-thread: 4096
include: /etc/unbound/a-records.conf
# forward-zone:
# name: "."
# forward-addr: 194.242.2.3@853 # Mullvad primary
# forward-addr: 193.19.108.3@853 # Mullvad secondary

View file

@ -0,0 +1,39 @@
version: "3.7"
services:
unbound:
image: "klutchell/unbound"
container_name: unbound
restart: unless-stopped
volumes:
- "${APP_DATA_DIR}/data/unbound:/etc/unbound"
networks:
tipi_main_network:
ipv4_address: 10.21.21.200
pihole:
depends_on: [unbound]
container_name: pihole
image: pihole/pihole:latest
restart: unless-stopped
hostname: pihole
dns:
- 127.0.0.1
- 10.21.21.200 # Points to unbound
ports:
- 53:53/tcp
- 53:53/udp
- ${APP_PORT}:80
volumes:
- ${APP_DATA_DIR}/data/pihole:/etc/pihole
- ${APP_DATA_DIR}/data/dnsmasq:/etc/dnsmasq.d
environment:
TZ: ${TZ}
WEBPASSWORD: ${APP_PASSWORD}
PIHOLE_DNS_: 10.21.21.200 # Points to unbound
FTLCONF_REPLY_ADDR4: 10.21.21.201
cap_add:
- NET_ADMIN
networks:
tipi_main_network:
ipv4_address: 10.21.21.201

20
apps/radarr/config.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "Radarr",
"port": 8088,
"id": "radarr",
"description": "",
"short_desc": "",
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/25025331?s=200&v=4",
"form_fields": {
"torrent-client": {
"type": "text",
"label": "Torrent Client",
"max": 50,
"min": 3,
"required": true,
"env_variable": "TORRENT_CLIENT"
}
}
}

View file

@ -0,0 +1,20 @@
version: "3.7"
services:
radarr:
image: lscr.io/linuxserver/radarr
container_name: radarr
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
dns:
- ${DNS_IP}
volumes:
- ${APP_DATA_DIR}/data:/config
- ${ROOT_FOLDER}/media/data/movies:/movies #optional
- ${ROOT_FOLDER}/media/torrents:/downloads #optional
ports:
- ${APP_PORT}:7878
restart: unless-stopped
networks:
- tipi_main_network

View file

@ -0,0 +1,11 @@
{
"name": "Simple Torrent",
"port": 8085,
"id": "simple-torrent",
"description": "SimpleTorrent is a a self-hosted remote torrent client, written in Go (golang). Started torrents remotely, download sets of files on the local disk of the server, which are then retrievable or streamable via HTTP.",
"short_desc": "A self-hosted remote torrent client",
"author": "",
"source": "https://github.com/boypt/simple-torrent",
"image": "https://getumbrel.github.io/umbrel-apps-gallery/simple-torrent/icon.svg",
"form_fields": {}
}

View file

@ -6,9 +6,9 @@ services:
image: boypt/cloud-torrent:1.3.9
restart: on-failure
ports:
- "${APP_SIMPLETORRENT_PORT}:${APP_SIMPLETORRENT_PORT}"
- ${APP_PORT}:${APP_PORT}
command: >
--port=${APP_SIMPLETORRENT_PORT}
--port=${APP_PORT}
--config-path /config/simple-torrent.json
volumes:
- ${APP_DATA_DIR}/data/torrents:/torrents

13
apps/sonarr/config.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "Sonarr",
"port": 8098,
"id": "sonarr",
"description": "",
"short_desc": "",
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/1082903?s=200&v=4",
"form_fields": {
}
}

View file

@ -0,0 +1,20 @@
version: "3.7"
services:
radarr:
image: lscr.io/linuxserver/sonarr
container_name: sonarr
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
dns:
- ${DNS_IP}
volumes:
- ${APP_DATA_DIR}/data:/config
- ${ROOT_FOLDER}/media/data/tv:/tv #optional
- ${ROOT_FOLDER}/media/torrents:/downloads #optional
ports:
- ${APP_PORT}:8989
restart: unless-stopped
networks:
- tipi_main_network

View file

@ -0,0 +1,12 @@
{
"name": "Syncthing",
"port": 8090,
"id": "syncthing",
"description": "Syncthing is a peer-to-peer continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.\n\nInstall the Syncthing app on your Umbrel and pair it with the Syncthing app on your phone or computer for a self hosted peer-to-peer backup solution.",
"short_desc": "Peer-to-peer file synchronization between your devices",
"author": "The Syncthing Foundation",
"source": "https://github.com/syncthing",
"website": "https://syncthing.net",
"image": "https://avatars.githubusercontent.com/u/7628018?s=200&v=4",
"form_fields": {}
}

View file

@ -0,0 +1,21 @@
version: "3.7"
services:
syncthing:
container_name: syncthing
image: syncthing/syncthing:1.19
stop_grace_period: 1m
hostname: tipi
environment:
- PUID=1000
- PGID=1000
volumes:
- ${APP_DATA_DIR}/data:/var/syncthing
ports:
- ${APP_PORT}:8384
- 22000:22000/tcp # TCP file transfers
- 22000:22000/udp # QUIC file transfers
- 21027:21027/udp # Receive local discovery broadcasts
restart: unless-stopped
networks:
- tipi_main_network

View file

@ -0,0 +1,12 @@
{
"name": "Tailscale",
"port": 8093,
"id": "tailscale",
"description": "",
"short_desc": "",
"author": "",
"source": "https://github.com/tailscale/tailscale",
"website": "https://tailscale.com/",
"image": "https://avatars.githubusercontent.com/u/48932923?s=200&v=4",
"form_fields": {}
}

View file

@ -0,0 +1,14 @@
version: "2"
services:
tailscale:
container_name: tailscale
network_mode: "host" # TODO: Find a way to remove this
image: tailscale/tailscale:v1.24.0
privileged: true
restart: on-failure
stop_grace_period: 1m
command: "sh -c 'tailscale web --listen 0.0.0.0:${APP_PORT} & exec tailscaled --tun=userspace-networking'"
volumes:
- /var/lib:/var/lib
- /dev/net/tun:/dev/net/tun

View file

@ -1,6 +0,0 @@
FROM ubuntu:latest
# Install curl
RUN apt-get update && apt-get install -y curl
ENTRYPOINT ["tail", "-f", "/dev/null"]

View file

@ -1,8 +0,0 @@
version: '3.7'
services:
test:
build:
context: .
dockerfile: Dockerfile
networks:
- tipi_main_network

View file

@ -0,0 +1,31 @@
{
"name": "Transmission",
"port": 8089,
"requirements": {
"ports": [51413]
},
"id": "transmission",
"description": "",
"short_desc": "",
"author": "",
"source": "https://transmissionbt.com",
"image": "https://avatars.githubusercontent.com/u/223312?s=200&v=4",
"form_fields": {
"username": {
"type": "text",
"label": "Username",
"max": 50,
"min": 3,
"required": true,
"env_variable": "TRANSMISSION_USERNAME"
},
"password": {
"type": "password",
"label": "Password",
"max": 50,
"min": 3,
"required": true,
"env_variable": "TRANSMISSION_PASSWORD"
}
}
}

View file

@ -0,0 +1,24 @@
version: "3.7"
services:
transmission:
image: lscr.io/linuxserver/transmission
container_name: transmission
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
- USER=${TRANSMISSION_USERNAME}
- PASS=${TRANSMISSION_PASSWORD}
# - WHITELIST=iplist #optional
# - PEERPORT=peerport #optional
# - HOST_WHITELIST=dnsnane list #optional
volumes:
- ${APP_DATA_DIR}/data/config:/config
- ${ROOT_FOLDER}/media/torrents:/downloads
ports:
- ${APP_PORT}:9091
- 51413:51413
- 51413:51413/udp
restart: unless-stopped
networks:
- tipi_main_network

29
apps/ttyd/config.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "TTYD - Web terminal",
"port": 8092,
"id": "ttyd",
"description": "",
"short_desc": "A utility that allows you to access a command line from your web browser",
"author": "",
"source": "",
"image": "",
"form_fields": {
"username": {
"type": "text",
"label": "Username",
"max": 50,
"min": 3,
"required": true,
"env_variable": "TRANSMISSION_USERNAME"
},
"password": {
"type": "password",
"label": "Password",
"max": 50,
"min": 3,
"required": true,
"env_variable": "TRANSMISSION_PASSWORD"
}
}
}

View file

35
apps/wg-easy/config.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "Wireguard",
"port": 8082,
"requirements": {
"ports": [51820]
},
"id": "wg-easy",
"description": "Access your homeserver from anywhere even on your mobile device. Wireguard-easy is a simple tool to configure and manage Wireguard VPN servers. It is written in Go and uses the official Wireguard client. You have to open and redirect port 51820 to your homeserver in order to connect.",
"short_desc": "VPN server for your homeserver",
"author": "WeeJeWel",
"source": "https://github.com/WeeJeWel/wg-easy/",
"image": "https://avatars.githubusercontent.com/u/13991055?s=200&v=4",
"form_fields": {
"host": {
"type": "fqdnip",
"label": "Your public IP address or domain name",
"required": true,
"env_variable": "WIREGUARD_HOST"
},
"password": {
"type": "password",
"label": "Password",
"max": 50,
"min": 3,
"required": true,
"env_variable": "WIREGUARD_PASSWORD"
},
"dns": {
"type": "ip",
"label": "Default DNS server",
"required": false,
"env_variable": "WIREGUARD_DNS"
}
}
}

View file

View file

@ -1,29 +1,36 @@
version: '3.7'
version: "3.7"
services:
wg-easy:
container_name: wg-easy
image: 'weejewel/wg-easy:latest'
restart: unless-stopped
volumes:
- ${APP_DATA_DIR}:/etc/wireguard
ports:
- 51820:51820
- ${APP_WGEASY_PORT}:51821
environment:
WG_HOST: '${WIREGUARD_HOST}'
PASSWORD: '${WIREGUARD_PASSWORD}'
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
networks:
- tipi_main_network
# labels:
# traefik.enable: true
# traefik.http.routers.wireguard.rule: Host(`wireguard.tipi.home`)
# traefik.http.routers.wireguard.service: wireguard
# traefik.http.routers.wireguard.tls: true
# traefik.http.routers.wireguard.entrypoints: websecure
# traefik.http.services.wireguard.loadbalancer.server.port: 51821
container_name: wg-easy
image: "meienberger/wg-easy:latest"
restart: unless-stopped
volumes:
- ${APP_DATA_DIR}/data:/etc/wireguard
- /lib/modules:/lib/modules
ports:
- 51822:51820/udp
- ${APP_PORT}:51821/tcp
environment:
WG_HOST: "${WIREGUARD_HOST}"
PASSWORD: "${WIREGUARD_PASSWORD}"
WG_ALLOWED_IPS: 0.0.0.0/0,::/0
WG_PORT: 51822
WG_DEFAULT_DNS: "${WIREGUARD_DNS:-8.8.8.8}"
WG_FWMARK: 51820
cap_add:
- NET_ADMIN
- SYS_MODULE
dns:
- "${WIREGUARD_DNS:-8.8.8.8}"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
networks:
- tipi_main_network
# labels:
# traefik.enable: true
# traefik.http.routers.wireguard.rule: Host(`wireguard.tipi.home`)
# traefik.http.routers.wireguard.service: wireguard
# traefik.http.routers.wireguard.tls: true
# traefik.http.routers.wireguard.entrypoints: websecure
# traefik.http.services.wireguard.loadbalancer.server.port: 51821

2
dashboard/.eslintignore Normal file
View file

@ -0,0 +1,2 @@
*.config.js
.eslintrc.js

17
dashboard/.eslintrc.js Normal file
View file

@ -0,0 +1,17 @@
module.exports = {
extends: ['next/core-web-vitals', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'import'],
rules: {
'arrow-body-style': 0,
'no-restricted-exports': 0,
'max-len': [1, { code: 200 }],
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
},
};

View file

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

6
dashboard/.prettierrc.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
singleQuote: true,
semi: true,
trailingComma: "all",
printWidth: 200,
};

View file

@ -9,6 +9,9 @@ RUN yarn
COPY ./ ./
ARG INTERNAL_IP_ARG
ENV INTERNAL_IP $INTERNAL_IP_ARG
RUN yarn build
CMD ["yarn", "start"]

12
dashboard/Dockerfile.dev Normal file
View file

@ -0,0 +1,12 @@
FROM node:latest
WORKDIR /app
COPY ./package.json ./
COPY ./yarn.lock ./
RUN yarn
COPY ./ ./
CMD ["yarn", "dev"]

View file

@ -1,6 +1,11 @@
/** @type {import('next').NextConfig} */
const { NODE_ENV, INTERNAL_IP } = process.env;
const nextConfig = {
reactStrictMode: true,
}
env: {
INTERNAL_IP: NODE_ENV === 'development' ? 'localhost' : INTERNAL_IP,
},
};
module.exports = nextConfig
module.exports = nextConfig;

View file

@ -12,20 +12,36 @@
"@chakra-ui/react": "^1.8.7",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@fontsource/open-sans": "^4.5.8",
"axios": "^0.26.1",
"clsx": "^1.1.1",
"final-form": "^4.20.6",
"framer-motion": "^6",
"immer": "^9.0.12",
"js-cookie": "^3.0.1",
"next": "12.1.4",
"react": "18.0.0",
"react-dom": "18.0.0",
"react-final-form": "^6.5.9",
"react-icons": "^4.3.1",
"swr": "^1.3.0",
"systeminformation": "^5.11.9",
"validator": "^13.7.0"
"validator": "^13.7.0",
"zustand": "^3.7.2"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
"@types/node": "17.0.23",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"autoprefixer": "^10.4.4",
"eslint": "8.12.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "12.1.4",
"postcss": "^8.4.12",
"tailwindcss": "^3.0.23",
"typescript": "4.6.3"
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2263 5057 c-67 -34 -125 -65 -128 -68 -3 -4 53 -126 125 -271 l132
-263 -122 -275 c-66 -151 -130 -295 -142 -320 -11 -25 -37 -83 -58 -130 -21
-47 -347 -706 -725 -1465 -378 -759 -786 -1578 -906 -1820 l-219 -440 2337 -3
c1285 -1 2338 0 2340 2 2 2 -397 809 -888 1792 -706 1417 -931 1879 -1085
2224 l-194 434 134 267 133 267 -135 66 c-103 51 -137 64 -143 54 -5 -7 -41
-79 -81 -160 -40 -81 -75 -147 -78 -148 -3 0 -27 44 -54 98 -101 203 -111 222
-116 221 -3 0 -60 -29 -127 -62z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1,003 B

View file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
dashboard/public/tipi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,23 @@
import React from 'react';
import { FiPauseCircle, FiPlayCircle } from 'react-icons/fi';
import { AppStatus as TAppStatus } from '../../core/types';
const AppStatus: React.FC<{ status: TAppStatus }> = ({ status }) => {
if (status === 'running') {
return (
<>
<FiPlayCircle className="text-green-500 mr-1" size={20} />
<span className="text-gray-400 text-sm">Running</span>
</>
);
}
return (
<>
<FiPauseCircle className="text-red-500 mr-1" size={20} />
<span className="text-gray-400 text-sm">Stopped</span>
</>
);
};
export default AppStatus;

View file

@ -0,0 +1,32 @@
import { Box, SlideFade, Image, useColorModeValue } from '@chakra-ui/react';
import Link from 'next/link';
import React from 'react';
import { FiChevronRight } from 'react-icons/fi';
import { AppConfig } from '../../core/types';
import AppStatus from './AppStatus';
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
const bg = useColorModeValue('white', '#1a202c');
return (
<Link href={`/apps/${app.id}`} passHref>
<SlideFade in className="flex flex-1" offsetY="20px">
<Box minWidth={400} bg={bg} className="flex flex-1 border-2 drop-shadow-sm rounded-lg p-3 items-center cursor-pointer group hover:drop-shadow-md transition-all">
<Image alt={`${app.name} logo`} className="rounded-md drop-shadow mr-3 group-hover:scale-105 transition-all" src={app.image} width={100} height={100} />
<div className="mr-3 flex-1">
<h3 className="font-bold text-xl">{app.name}</h3>
<span>{app.short_desc}</span>
{app.installed && (
<div className="flex mt-1">
<AppStatus status={app.status} />
</div>
)}
</div>
<FiChevronRight className="text-slate-300" size={30} />
</Box>
</SlideFade>
</Link>
);
};
export default AppTile;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { Input } from '@chakra-ui/react';
import clsx from 'clsx';
interface IProps {
placeholder?: string;
error?: string;
type?: Parameters<typeof Input>[0]['type'];
label?: string;
className?: string;
isInvalid?: boolean;
size?: Parameters<typeof Input>[0]['size'];
}
const FormInput: React.FC<IProps> = ({ placeholder, error, type, label, className, isInvalid, size, ...rest }) => {
return (
<div className={clsx('transition-all', className)}>
{label && <label>{label}</label>}
<Input type={type} placeholder={placeholder} isInvalid={isInvalid} size={size} {...rest} />
{isInvalid && <span className="text-red-500 text-sm">{error}</span>}
</div>
);
};
export default FormInput;

View file

@ -0,0 +1,28 @@
import validator from 'validator';
import { AppConfig, FieldTypes } from '../../core/types';
export const validateAppConfig = (values: Record<string, string>, fields: (AppConfig['form_fields'][0] & { id: string })[]) => {
const errors: any = {};
fields.forEach((field) => {
if (field.required && !values[field.id]) {
errors[field.id] = 'Field required';
} else if (values[field.id] && field.min && values[field.id].length < field.min) {
errors[field.id] = `Field must be at least ${field.min} characters long`;
} else if (values[field.id] && field.max && values[field.id].length > field.max) {
errors[field.id] = `Field must be at most ${field.max} characters long`;
} else if (values[field.id] && field.type === FieldTypes.number && !validator.isNumeric(values[field.id])) {
errors[field.id] = 'Field must be a number';
} else if (values[field.id] && field.type === FieldTypes.email && !validator.isEmail(values[field.id])) {
errors[field.id] = 'Field must be a valid email';
} else if (values[field.id] && field.type === FieldTypes.fqdn && !validator.isFQDN(values[field.id] || '')) {
errors[field.id] = 'Field must be a valid domain';
} else if (values[field.id] && field.type === FieldTypes.ip && !validator.isIP(values[field.id])) {
errors[field.id] = 'Field must be a valid IP address';
} else if (values[field.id] && field.type === FieldTypes.fqdnip && !validator.isFQDN(values[field.id] || '') && !validator.isIP(values[field.id])) {
errors[field.id] = 'Field must be a valid domain or IP address';
}
});
return errors;
};

View file

@ -1,27 +1,22 @@
import React from "react";
import Img from "next/image";
import Link from "next/link";
import { Button, Flex, useBreakpointValue } from "@chakra-ui/react";
import React from 'react';
import Link from 'next/link';
import { Flex } from '@chakra-ui/react';
import { FiMenu } from 'react-icons/fi';
interface IProps {
onClickMenu: () => void;
}
const Header: React.FC<IProps> = ({ onClickMenu }) => {
const buttonVisibility = useBreakpointValue<"visible" | "hidden">({
base: "visible",
md: "hidden",
});
return (
<header>
<Flex alignItems="center" bg="tomato" paddingLeft={5} paddingRight={5}>
<Flex position="absolute" visibility={buttonVisibility || "visible"}>
<Button onClick={onClickMenu}>O</Button>
</Flex>
<header style={{ width: '100%' }} className="flex h-12 md:h-0">
<Flex className="items-center border-b-2 bg-graycool px-5 flex-1 py-2">
<div onClick={onClickMenu} className="visible md:invisible absolute cursor-pointer py-2">
<FiMenu color="black" />
</div>
<Flex justifyContent="center" flex="1">
<Link href="/" passHref>
<Img src="/logo.svg" alt="Tipi" width={100} height={60} />
<img src="/tipi.png" alt="Tipi Logo" width={30} height={30} />
</Link>
</Flex>
</Flex>

View file

@ -1,33 +1,70 @@
import {
Button,
Flex,
useBreakpointValue,
useDisclosure,
} from "@chakra-ui/react";
import React from "react";
import Header from "./Header";
import Menu from "./Menu";
import MenuDrawer from "./MenuDrawer";
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
import Head from 'next/head';
import Link from 'next/link';
import React from 'react';
import { FiChevronRight } from 'react-icons/fi';
import Header from './Header';
import Menu from './Menu';
import MenuDrawer from './MenuDrawer';
const Layout: React.FC = ({ children }) => {
const menuWidth = useBreakpointValue({ base: 0, md: 200 });
const { isOpen, onOpen, onClose } = useDisclosure();
interface IProps {
loading?: boolean;
breadcrumbs?: { name: string; href: string; current?: boolean }[];
}
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const menubg = useColorModeValue('#F1F3F4', '#202736');
const bg = useColorModeValue('white', '#1a202c');
const renderContent = () => {
if (loading) {
return (
<Flex className="justify-center flex-1">
<Spinner />
</Flex>
);
}
return children;
};
const renderBreadcrumbs = () => {
return (
<Breadcrumb spacing="8px" separator={<FiChevronRight color="gray.500" />}>
{breadcrumbs?.map((breadcrumb, index) => {
return (
<BreadcrumbItem className="hover:underline" isCurrentPage={breadcrumb.current} key={index}>
<Link href={breadcrumb.href}>{breadcrumb.name}</Link>
</BreadcrumbItem>
);
})}
</Breadcrumb>
);
};
return (
<Flex height="100vh" bg="green.500" direction="column">
<MenuDrawer isOpen={isOpen} onClose={onClose}>
<Menu />
</MenuDrawer>
<Header onClickMenu={onOpen} />
<Flex flex="1">
<Flex width={menuWidth} bg="blue.500">
<>
<Head>
<title>Tipi</title>
</Head>
<Flex height="100vh" direction="column">
<MenuDrawer isOpen={isOpen} onClose={onClose}>
<Menu />
</Flex>
<Flex flex="1" padding={5} bg="yellow.300">
{children}
</MenuDrawer>
<Header onClickMenu={onOpen} />
<Flex flex={1}>
<Flex height="100vh" bg={menubg} className="sticky top-0 invisible md:visible w-0 md:w-64">
<Menu />
</Flex>
<Box bg={bg} className="flex-1 px-4 py-4 md:px-10 md:py-8">
{renderBreadcrumbs()}
{renderContent()}
</Box>
</Flex>
</Flex>
</Flex>
</>
);
};

View file

@ -1,11 +1,71 @@
import React from "react";
import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-icons/ai';
import { FaRegMoon } from 'react-icons/fa';
import { FiLogOut } from 'react-icons/fi';
import Package from '../../../package.json';
import { Box, Divider, Flex, List, ListItem, Switch, useColorMode } from '@chakra-ui/react';
import React from 'react';
import Link from 'next/link';
import clsx from 'clsx';
import { useRouter } from 'next/router';
import { IconType } from 'react-icons';
import { useAuthStore } from '../../state/authStore';
const SideMenu: React.FC = () => {
const router = useRouter();
const { colorMode, setColorMode } = useColorMode();
const { logout } = useAuthStore();
const path = router.pathname.split('/')[1];
const renderMenuItem = (title: string, name: string, Icon: IconType) => {
const selected = path === name;
const itemClass = clsx('mx-3 border-transparent rounded-lg p-3 transition-colors border-1', {
'drop-shadow-sm border-gray-200': selected && colorMode === 'light',
'bg-white': selected && colorMode === 'light',
});
return (
<Link href={`/${name}`} passHref>
<div className={itemClass}>
<ListItem className={'flex items-center cursor-pointer hover:font-bold'}>
<Icon size={20} className={clsx('mr-3', { 'text-red-600': selected && colorMode === 'light', 'text-red-200': selected && colorMode === 'dark' })} />
<p className={clsx({ 'font-bold': selected, 'text-red-600': selected && colorMode === 'light', 'text-red-200': selected && colorMode === 'dark' })}>{title}</p>
</ListItem>
</div>
</Link>
);
};
const handleChangeColorMode = (checked: boolean) => {
setColorMode(checked ? 'dark' : 'light');
};
const Menu: React.FC = () => {
return (
<div>
<h1>Menu</h1>
</div>
<Box className="flex-1 flex flex-col p-0 md:p-4">
<img className="self-center mb-5 logo mt-0 md:mt-5" src="/tipi.png" width={512} height={512} />
<List spacing={3} className="pt-5">
{renderMenuItem('Dashboard', '', AiOutlineDashboard)}
{renderMenuItem('Apps', 'apps', AiOutlineAppstore)}
{renderMenuItem('Settings', 'settings', AiOutlineSetting)}
</List>
<Divider className="my-3" />
<Flex flex="1" />
<List>
<div className="mx-3">
<ListItem onClick={logout} className="cursor-pointer hover:font-bold flex items-center mb-5">
<FiLogOut size={20} className="mr-3" />
<p className="flex-1">Log out</p>
</ListItem>
<ListItem className="flex items-center">
<FaRegMoon size={20} className="mr-3" />
<p className="flex-1">Dark mode</p>
<Switch checked={colorMode === 'dark'} onChange={(event) => handleChangeColorMode(event.target.checked)} />
</ListItem>
</div>
</List>
<div className="pb-1 text-center text-sm text-gray-400 mt-5">Tipi version {Package.version}</div>
</Box>
);
};
export default Menu;
export default SideMenu;

View file

@ -1,13 +1,5 @@
import {
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
} from "@chakra-ui/react";
import React from "react";
import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface IProps {
isOpen: boolean;
@ -15,16 +7,15 @@ interface IProps {
}
const MenuDrawer: React.FC<IProps> = ({ children, isOpen, onClose }) => {
const menubg = useColorModeValue('#F1F3F4', '#202736');
return (
<Drawer isOpen={isOpen} placement="left" onClose={onClose}>
<Drawer size="xs" isOpen={isOpen} placement="left" onClose={onClose}>
<DrawerOverlay />
<DrawerContent>
<DrawerContent bg={menubg}>
<DrawerCloseButton />
<DrawerHeader>Create your account</DrawerHeader>
<DrawerBody>{children}</DrawerBody>
<DrawerFooter>
<div>Github</div>
</DrawerFooter>
<DrawerHeader>My Tipi</DrawerHeader>
<DrawerBody display="flex">{children}</DrawerBody>
</DrawerContent>
</Drawer>
);

View file

@ -1 +1 @@
export { default } from "./Layout";
export { default } from './Layout';

View file

@ -0,0 +1,12 @@
import { Flex, Spinner } from '@chakra-ui/react';
import React from 'react';
const LoadingScreen = () => {
return (
<Flex height="100vh" alignItems="center" justifyContent="center">
<Spinner size="lg" />
</Flex>
);
};
export default LoadingScreen;

View file

@ -1,4 +1,4 @@
import validator from "validator";
import validator from 'validator';
interface IFormField {
name: string;
@ -20,81 +20,79 @@ interface IAppConfig {
}
const APP_ANONADDY: IAppConfig = {
id: "anonaddy",
name: "Anonaddy",
description: "Create Unlimited Email Aliases For Free",
url: "https://anonaddy.com/",
color: "#00a8ff",
logo: "https://anonaddy.com/favicon.ico",
id: 'anonaddy',
name: 'Anonaddy',
description: 'Create Unlimited Email Aliases For Free',
url: 'https://anonaddy.com/',
color: '#00a8ff',
logo: 'https://anonaddy.com/favicon.ico',
install_form: {
fields: [
{
name: "API Key",
type: "text",
placeholder: "API Key",
name: 'API Key',
type: 'text',
placeholder: 'API Key',
required: true,
validate: (value: string) => validator.isBase64(value),
},
{
name: "Return Path",
type: "text",
description: "The email address that bounces will be sent to",
placeholder: "Return Path",
name: 'Return Path',
type: 'text',
description: 'The email address that bounces will be sent to',
placeholder: 'Return Path',
required: false,
validate: (value: string) => validator.isEmail(value),
},
{
name: "Admin Username",
type: "text",
description: "The username of the admin user",
placeholder: "Admin Username",
name: 'Admin Username',
type: 'text',
description: 'The username of the admin user',
placeholder: 'Admin Username',
required: true,
},
{
name: "Enable Registration",
type: "boolean",
description: "Allow users to register",
placeholder: "Enable Registration",
name: 'Enable Registration',
type: 'boolean',
description: 'Allow users to register',
placeholder: 'Enable Registration',
required: false,
},
{
name: "Domain",
type: "text",
description: "The domain that will be used for the email address",
placeholder: "Domain",
name: 'Domain',
type: 'text',
description: 'The domain that will be used for the email address',
placeholder: 'Domain',
required: true,
validate: (value: string) => validator.isFQDN(value),
},
{
name: "Hostname",
type: "text",
description: "The hostname that will be used for the email address",
placeholder: "Hostname",
name: 'Hostname',
type: 'text',
description: 'The hostname that will be used for the email address',
placeholder: 'Hostname',
required: true,
validate: (value: string) => validator.isFQDN(value),
},
{
name: "Secret",
type: "text",
description: "The secret that will be used for the email address",
placeholder: "Secret",
name: 'Secret',
type: 'text',
description: 'The secret that will be used for the email address',
placeholder: 'Secret',
required: true,
},
{
name: "From Name",
type: "text",
description: "The name that will be used for the email address",
placeholder: "From Name",
name: 'From Name',
type: 'text',
description: 'The name that will be used for the email address',
placeholder: 'From Name',
required: true,
validate: (value: string) =>
validator.isLength(value, { min: 1, max: 64 }),
validate: (value: string) => validator.isLength(value, { min: 1, max: 64 }),
},
{
name: "From Address",
type: "text",
description:
"The email address that will be used for the email address",
placeholder: "From Address",
name: 'From Address',
type: 'text',
description: 'The email address that will be used for the email address',
placeholder: 'From Address',
required: true,
validate: (value: string) => validator.isEmail(value),
},

32
dashboard/src/core/api.ts Normal file
View file

@ -0,0 +1,32 @@
import axios, { Method } from 'axios';
export const BASE_URL = `http://${process.env.INTERNAL_IP}:3001`;
interface IFetchParams {
endpoint: string;
method?: Method;
params?: JSON;
data?: Record<string, unknown>;
}
const api = async <T = unknown>(fetchParams: IFetchParams): Promise<T> => {
const { endpoint, method = 'GET', params, data } = fetchParams;
const response = await axios.request<T & { error?: string }>({
method,
params,
data,
url: `${BASE_URL}${endpoint}`,
withCredentials: true,
});
if (response.data.error) {
throw new Error(response.data.error);
}
if (response.data) return response.data;
throw new Error(`Network request error. status : ${response.status}`);
};
export default { fetch: api };

View file

@ -0,0 +1,9 @@
import { BareFetcher } from 'swr';
import axios from 'axios';
import { BASE_URL } from './api';
const fetcher: BareFetcher<any> = (url: string) => {
return axios.get(url, { baseURL: BASE_URL }).then((res) => res.data);
};
export default fetcher;

View file

@ -0,0 +1,57 @@
export enum FieldTypes {
text = 'text',
password = 'password',
email = 'email',
number = 'number',
fqdn = 'fqdn',
ip = 'ip',
fqdnip = 'fqdnip',
}
interface FormField {
type: FieldTypes;
label: string;
max?: number;
min?: number;
hint?: string;
required?: boolean;
env_variable: string;
}
export interface AppConfig {
id: string;
port: number;
requirements?: {
ports?: number[];
};
name: string;
description: string;
version: string;
image: string;
form_fields: Record<string, FormField>;
short_desc: string;
author: string;
source: string;
installed: boolean;
status: AppStatus;
}
export enum RequestStatus {
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
LOADING = 'LOADING',
}
export enum AppStatus {
RUNNING = 'running',
STOPPED = 'stopped',
INSTALLING = 'installing',
UNINSTALLING = 'uninstalling',
STOPPING = 'stopping',
STARTING = 'starting',
}
export interface IUser {
name: string;
email: string;
}

View file

@ -0,0 +1,70 @@
import { Button } from '@chakra-ui/react';
import React from 'react';
import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
import { AppConfig, AppStatus } from '../../../core/types';
interface IProps {
app: AppConfig;
onInstall: () => void;
onUninstall: () => void;
onStart: () => void;
onStop: () => void;
onOpen: () => void;
onUpdate: () => void;
}
const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
const hasSettings = Object.keys(app.form_fields).length > 0;
if (app?.installed && app.status === AppStatus.STOPPED) {
return (
<div className="flex flex-wrap justify-center">
<Button onClick={onStart} width={160} colorScheme="green" className="mt-3 mr-2">
Start
<FiPlay className="ml-1" />
</Button>
<Button onClick={onUninstall} width={160} colorScheme="gray" className="mt-3 mr-2">
Remove
<FiTrash2 className="ml-1" />
</Button>
{hasSettings && (
<Button onClick={onUpdate} width={160} colorScheme="gray" className="mt-3 mr-2">
Settings
<FiSettings className="ml-1" />
</Button>
)}
</div>
);
} else if (app?.installed && app.status === AppStatus.RUNNING) {
return (
<div>
<Button onClick={onOpen} width={160} colorScheme="gray" className="mt-3 mr-2">
Open
<FiExternalLink className="ml-1" />
</Button>
<Button onClick={onStop} width={160} colorScheme="red" className="mt-3">
Stop
<FiPause className="ml-2" />
</Button>
</div>
);
} else if (app.status === AppStatus.INSTALLING || app.status === AppStatus.UNINSTALLING || app.status === AppStatus.STARTING || app.status === AppStatus.STOPPING) {
return (
<div className="flex items-center flex-col md:flex-row">
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
Install
<FiPlay className="ml-1" />
</Button>
<span className="text-gray-500 text-sm ml-2 mt-3">{`App is ${app.status} please wait and don't refresh page...`}</span>
</div>
);
}
return (
<Button onClick={onInstall} width={160} colorScheme="green" className="mt-3">
Install
</Button>
);
};
export default AppActions;

Some files were not shown because too many files have changed in this diff Show more