Nicolas Meienberger 3 лет назад
Родитель
Сommit
f0f1da36ae
65 измененных файлов с 333 добавлено и 94 удалено
  1. 6 4
      .dockerignore
  2. 5 14
      .github/workflows/release-candidate.yml
  3. 3 13
      .github/workflows/release.yml
  4. 1 0
      .gitignore
  5. 61 0
      Dockerfile
  6. 43 0
      Dockerfile.dev
  7. 1 1
      apps/adguard/config.json
  8. BIN
      apps/adguard/metadata/logo.png
  9. 0 0
      apps/anonaddy/metadata/description.md
  10. 0 0
      apps/calibre-web/metadata/description.md
  11. 0 0
      apps/code-server/metadata/description.md
  12. 0 0
      apps/filebrowser/metadata/description.md
  13. 0 0
      apps/filerun/metadata/description.md
  14. 0 0
      apps/freshrss/metadata/description.md
  15. 0 0
      apps/gitea/metadata/description.md
  16. 0 0
      apps/homarr/metadata/description.md
  17. 0 0
      apps/homeassistant/metadata/description.md
  18. 0 0
      apps/invidious/metadata/description.md
  19. 0 0
      apps/jackett/metadata/description.md
  20. 0 0
      apps/jellyfin/metadata/description.md
  21. 0 0
      apps/joplin/metadata/description.md
  22. 0 0
      apps/libreddit/metadata/description.md
  23. 0 0
      apps/n8n/metadata/description.md
  24. 0 0
      apps/nextcloud/metadata/description.md
  25. 0 0
      apps/nitter/metadata/description.md
  26. 0 0
      apps/nodered/metadata/description.md
  27. 0 0
      apps/photoprism/metadata/description.md
  28. 0 0
      apps/pihole/metadata/description.md
  29. 0 0
      apps/prowlarr/metadata/description.md
  30. 0 0
      apps/radarr/metadata/description.md
  31. 0 0
      apps/simple-torrent/metadata/description.md
  32. 0 0
      apps/sonarr/metadata/description.md
  33. 0 0
      apps/syncthing/metadata/description.md
  34. 1 0
      apps/tailscale/config.json
  35. 0 0
      apps/tailscale/metadata/description.md
  36. 0 0
      apps/tautulli/metadata/description.md
  37. 0 0
      apps/transmission/metadata/description.md
  38. 0 0
      apps/ttyd/metadata/description.md
  39. 0 0
      apps/vaultwarden/metadata/description.md
  40. 0 0
      apps/wg-easy/metadata/description.md
  41. 9 7
      docker-compose.dev.yml
  42. 4 2
      docker-compose.yml
  43. 2 1
      package.json
  44. 2 2
      packages/common/package.json
  45. 1 0
      packages/common/src/constants/app.constants.ts
  46. 1 0
      packages/common/src/types/app.types.ts
  47. 2 1
      packages/dashboard/.eslintignore
  48. 0 13
      packages/dashboard/Dockerfile
  49. 0 11
      packages/dashboard/Dockerfile.dev
  50. 12 0
      packages/dashboard/jest.config.js
  51. 3 1
      packages/dashboard/package.json
  52. 3 2
      packages/dashboard/src/components/Layout/Menu.tsx
  53. 25 0
      packages/dashboard/src/modules/AppStore/components/FeaturedApps.tsx
  54. 36 0
      packages/dashboard/src/modules/AppStore/components/FeaturedCard.tsx
  55. 24 0
      packages/dashboard/src/modules/AppStore/containers/AppStoreContainer.tsx
  56. 22 0
      packages/dashboard/src/pages/app-store/index.tsx
  57. 4 1
      packages/system-api/.gitignore
  58. 14 4
      packages/system-api/Dockerfile
  59. 5 0
      packages/system-api/jest.config.cjs
  60. 2 2
      packages/system-api/package.json
  61. 6 2
      packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
  62. 2 2
      packages/system-api/src/modules/apps/apps.service.ts
  63. 2 2
      packages/system-api/src/modules/auth/__tests__/auth.controller.test.ts
  64. 0 4
      packages/system-api/src/modules/auth/auth.service.ts
  65. 31 5
      pnpm-lock.yaml

+ 6 - 4
.dockerignore

@@ -1,4 +1,6 @@
-**/node_modules/
-**/.next/
-/node_modules/
-/.next/
+**/node_modules
+**/.next
+/node_modules
+/.next
+node_modules
+.next

+ 5 - 14
.github/workflows/release-candidate.yml

@@ -61,22 +61,13 @@ jobs:
       - name: Build and push dashboard
         uses: docker/build-push-action@v3
         with:
-          context: ./packages/dashboard
+          context: .
           platforms: linux/amd64,linux/arm64,linux/arm/v7
           push: true
-          tags: meienberger/tipi-dashboard:rc-${{ steps.meta.outputs.TAG }}
-          cache-from: type=registry,ref=meienberger/tipi-dashboard:buildcache
-          cache-to: type=registry,ref=meienberger/tipi-dashboard:buildcache,mode=max
-      
-      - name: Build and push api
-        uses: docker/build-push-action@v3
-        with:
-          context: ./packages/system-api
-          platforms: linux/amd64,linux/arm64,linux/arm/v7
-          push: true
-          tags: meienberger/tipi-api:rc-${{ steps.meta.outputs.TAG }}
-          cache-from: type=registry,ref=meienberger/tipi-api:buildcache
-          cache-to: type=registry,ref=meienberger/tipi-api:buildcache,mode=max
+          tags: meienberger/runtipi:rc-${{ steps.meta.outputs.TAG }}
+          cache-from: type=registry,ref=meienberger/runtipi:buildcache
+          cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
+    
 
   # Test installation script
   # test-install:

+ 3 - 13
.github/workflows/release.yml

@@ -62,16 +62,6 @@ jobs:
           context: ./packages/dashboard
           platforms: linux/amd64,linux/arm64,linux/arm/v7
           push: true
-          tags: meienberger/tipi-dashboard:latest,meienberger/tipi-dashboard:${{ steps.meta.outputs.TAG }}
-          cache-from: type=registry,ref=meienberger/tipi-dashboard:buildcache
-          cache-to: type=registry,ref=meienberger/tipi-dashboard:buildcache,mode=max
-      
-      - name: Build and push api
-        uses: docker/build-push-action@v3
-        with:
-          context: ./packages/system-api
-          platforms: linux/amd64,linux/arm64,linux/arm/v7
-          push: true
-          tags: meienberger/tipi-api:latest,meienberger/tipi-api:${{ steps.meta.outputs.TAG }}
-          cache-from: type=registry,ref=meienberger/tipi-api:buildcache
-          cache-to: type=registry,ref=meienberger/tipi-api:buildcache,mode=max
+          tags: meienberger/runtipi:latest,meienberger/runtipi:${{ steps.meta.outputs.TAG }}
+          cache-from: type=registry,ref=meienberger/runtipi:buildcache
+          cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max

+ 1 - 0
.gitignore

@@ -25,3 +25,4 @@ media/torrents/incomplete/*
 !media/torrents/incomplete/.gitkeep
 media/torrents/watch/*
 !media/torrents/watch/.gitkeep
+packages/dashboard/package-lock.json

+ 61 - 0
Dockerfile

@@ -0,0 +1,61 @@
+FROM node:18-buster-slim AS build
+
+COPY ./packages/common /common
+
+WORKDIR /api
+COPY ./packages/system-api/package.json /api/package.json
+RUN npm i
+COPY ./packages/system-api /api
+RUN npm run build
+
+WORKDIR /dashboard
+COPY ./packages/dashboard/package.json /dashboard/package.json
+RUN npm i
+COPY ./packages/dashboard /dashboard
+RUN npm run build
+
+
+FROM ubuntu:20.04
+ARG DEBIAN_FRONTEND=noninteractive
+
+WORKDIR /
+
+# Install docker
+RUN apt-get update && apt-get install -y \
+    ca-certificates \
+    curl \
+    gnupg \
+    lsb-release
+
+RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+
+RUN echo \
+    "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
+    $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
+
+RUN apt-get update
+RUN apt-get install -y docker-ce docker-ce-cli containerd.io
+
+# Install node
+RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
+RUN apt-get install -y nodejs
+RUN npm install --quiet node-gyp -g
+
+# Install docker-compose
+RUN curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+RUN chmod +x /usr/local/bin/docker-compose
+
+COPY ./packages/common /common
+
+WORKDIR /api
+COPY ./packages/system-api/package.json /api/package.json
+RUN npm install --omit=dev
+
+WORKDIR /dashboard
+COPY ./packages/dashboard/package.json /dashboard/package.json
+RUN npm install --omit=dev
+
+COPY --from=build /api /api
+COPY --from=build /dashboard /dashboard
+
+WORKDIR /

+ 43 - 0
Dockerfile.dev

@@ -0,0 +1,43 @@
+FROM ubuntu:20.04
+ARG DEBIAN_FRONTEND=noninteractive
+
+WORKDIR /
+
+# Install docker
+RUN apt-get update && apt-get install -y \
+    ca-certificates \
+    curl \
+    gnupg \
+    lsb-release
+
+RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+
+RUN echo \
+    "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
+    $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
+
+RUN apt-get update
+RUN apt-get install -y docker-ce docker-ce-cli containerd.io
+
+# Install node
+RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
+RUN apt-get install -y nodejs
+
+# Install docker-compose
+RUN curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+RUN chmod +x /usr/local/bin/docker-compose
+
+COPY ./packages/common /common
+
+WORKDIR /api
+COPY ./packages/system-api/package.json /api/package.json
+RUN npm install
+
+WORKDIR /dashboard
+COPY ./packages/dashboard/package.json /dashboard/package.json
+RUN npm install
+
+COPY ./packages/system-api /api
+COPY ./packages/dashboard /dashboard
+
+WORKDIR /

+ 1 - 1
apps/adguard/config.json

@@ -3,7 +3,7 @@
   "available": true,
   "port": 8104,
   "id": "adguard",
-  "categories": ["network", "security"],
+  "categories": ["network", "security", "featured"],
   "description": "Adguard is the best way to get rid of annoying ads and online tracking and protect your computer from malware. Make your web surfing fast, safe and ad-free.",
   "short_desc": "World's most advanced adblocker!",
   "author": "ArneNaessens",

BIN
apps/adguard/metadata/logo.png


+ 0 - 0
apps/anonaddy/metadata/description.md


+ 0 - 0
apps/calibre-web/metadata/description.md


+ 0 - 0
apps/code-server/metadata/description.md


+ 0 - 0
apps/filebrowser/metadata/description.md


+ 0 - 0
apps/filerun/metadata/description.md


+ 0 - 0
apps/freshrss/metadata/description.md


+ 0 - 0
apps/gitea/metadata/description.md


+ 0 - 0
apps/homarr/metadata/description.md


+ 0 - 0
apps/homeassistant/metadata/description.md


+ 0 - 0
apps/invidious/metadata/description.md


+ 0 - 0
apps/jackett/metadata/description.md


+ 0 - 0
apps/jellyfin/metadata/description.md


+ 0 - 0
apps/joplin/metadata/description.md


+ 0 - 0
apps/libreddit/metadata/description.md


+ 0 - 0
apps/n8n/metadata/description.md


+ 0 - 0
apps/nextcloud/metadata/description.md


+ 0 - 0
apps/nitter/metadata/description.md


+ 0 - 0
apps/nodered/metadata/description.md


+ 0 - 0
apps/photoprism/metadata/description.md


+ 0 - 0
apps/pihole/metadata/description.md


+ 0 - 0
apps/prowlarr/metadata/description.md


+ 0 - 0
apps/radarr/metadata/description.md


+ 0 - 0
apps/simple-torrent/metadata/description.md


+ 0 - 0
apps/sonarr/metadata/description.md


+ 0 - 0
apps/syncthing/metadata/description.md


+ 1 - 0
apps/tailscale/config.json

@@ -3,6 +3,7 @@
   "available": true,
   "port": 8093,
   "id": "tailscale",
+  "categories": ["featured"],
   "description": "Zero config VPN. Installs on any device in minutes, manages firewall rules for you, and works from anywhere.",
   "short_desc": "The easiest, most secure way to use WireGuard and 2FA.",
   "author": "© Tailscale Inc.",

+ 0 - 0
apps/tailscale/metadata/description.md


+ 0 - 0
apps/tautulli/metadata/description.md


+ 0 - 0
apps/transmission/metadata/description.md


+ 0 - 0
apps/ttyd/metadata/description.md


+ 0 - 0
apps/vaultwarden/metadata/description.md


+ 0 - 0
apps/wg-easy/metadata/description.md


+ 9 - 7
docker-compose.dev.yml

@@ -3,8 +3,9 @@ version: "3.7"
 services:
   api:
     build:
-      context: ./packages/system-api
+      context: .
       dockerfile: Dockerfile.dev
+    command: bash -c "cd /api && npm run dev"
     container_name: api
     ports:
       - 3001:3001
@@ -12,8 +13,8 @@ services:
       ## Docker sock
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - ${PWD}:/tipi
-      - ${PWD}/packages/system-api:/app
-      - /app/node_modules
+      - ${PWD}/packages/system-api:/api
+      - /api/node_modules
     environment:
       - INTERNAL_IP=${INTERNAL_IP}
       - TIPI_VERSION=${TIPI_VERSION}
@@ -25,8 +26,9 @@ services:
 
   dashboard:
     build:
-      context: ./packages/dashboard
+      context: .
       dockerfile: Dockerfile.dev
+    command: bash -c "cd /dashboard && npm run dev"
     container_name: dashboard
     ports:
       - 3000:3000
@@ -35,9 +37,9 @@ services:
     environment:
       - INTERNAL_IP=${INTERNAL_IP}
     volumes:
-      - ${PWD}/packages/dashboard:/app
-      - /app/node_modules
-      - /app/.next
+      - ${PWD}/packages/dashboard:/dashboard
+      - /dashboard/node_modules
+      - /dashboard/.next
     labels:
       traefik.enable: true
       traefik.http.routers.dashboard.rule: PathPrefix("/") # Host(`tipi.local`) &&

+ 4 - 2
docker-compose.yml

@@ -16,7 +16,8 @@ services:
       - tipi_main_network
 
   api:
-    image: meienberger/tipi-api:${TIPI_VERSION}
+    image: meienberger/runtipi:${TIPI_VERSION}
+    command: bash -c "cd /api && npm run start"
     restart: unless-stopped
     container_name: api
     ports:
@@ -35,7 +36,8 @@ services:
       - tipi_main_network
 
   dashboard:
-    image: meienberger/tipi-dashboard:${TIPI_VERSION}
+    image: meienberger/runtipi:${TIPI_VERSION}
+    command: bash -c "cd /dasboard && npm run start"
     restart: unless-stopped
     container_name: dashboard
     ports:

+ 2 - 1
package.json

@@ -30,5 +30,6 @@
   "bugs": {
     "url": "https://github.com/meienberger/runtipi/issues"
   },
-  "homepage": "https://github.com/meienberger/runtipi#readme"
+  "homepage": "https://github.com/meienberger/runtipi#readme",
+  "dependencies": {}
 }

+ 2 - 2
packages/common/package.json

@@ -1,12 +1,12 @@
 {
   "name": "@runtipi/common",
-  "version": "0.2.7",
+  "version": "0.2.8",
   "main": "./dist/index.js",
   "files": [
     "dist"
   ],
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "test": "jest --coverage --passWithNoTests",
     "build": "tsc -b tsconfig.build.json"
   },
   "author": "",

+ 1 - 0
packages/common/src/constants/app.constants.ts

@@ -10,4 +10,5 @@ export const APP_CATEGORIES = [
   { name: 'Utilities', id: AppCategoriesEnum.UTILITIES, icon: 'FaWrench' },
   { name: 'Photography', id: AppCategoriesEnum.PHOTOGRAPHY, icon: 'FaCamera' },
   { name: 'Security', id: AppCategoriesEnum.SECURITY, icon: 'FaShieldAlt' },
+  { name: 'Featured', id: AppCategoriesEnum.FEATURED, icon: 'FaStar' },
 ];

+ 1 - 0
packages/common/src/types/app.types.ts

@@ -7,6 +7,7 @@ export enum AppCategoriesEnum {
   UTILITIES = 'utilities',
   PHOTOGRAPHY = 'photography',
   SECURITY = 'security',
+  FEATURED = 'featured',
 }
 
 export enum FieldTypes {

+ 2 - 1
packages/dashboard/.eslintignore

@@ -1,3 +1,4 @@
 *.config.js
 .eslintrc.js
-next.config.js
+next.config.js
+jest.config.js

+ 0 - 13
packages/dashboard/Dockerfile

@@ -1,13 +0,0 @@
-FROM node:18-buster-slim 
-
-WORKDIR /app
-
-COPY ./package.json ./
-
-RUN npm install
-
-COPY ./ ./
-
-RUN npm run build
-
-CMD ["npm", "run", "start"]

+ 0 - 11
packages/dashboard/Dockerfile.dev

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

+ 12 - 0
packages/dashboard/jest.config.js

@@ -0,0 +1,12 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+  preset: 'ts-jest',
+  verbose: true,
+  // testEnvironment: 'node',
+  testMatch: ['**/__tests__/**/*.test.ts'],
+  // setupFiles: ['<rootDir>/tests/dotenv-config.ts'],
+  collectCoverage: true,
+  collectCoverageFrom: ['src/**/*.{ts,tsx}'],
+  coverageProvider: 'v8',
+  passWithNoTests: true,
+};

+ 3 - 1
packages/dashboard/package.json

@@ -3,6 +3,7 @@
   "version": "0.2.1",
   "private": true,
   "scripts": {
+    "test": "jest --colors",
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
@@ -13,7 +14,7 @@
     "@emotion/react": "^11",
     "@emotion/styled": "^11",
     "@fontsource/open-sans": "^4.5.8",
-    "@runtipi/common": "^0.2.7",
+    "@runtipi/common": "file:../common",
     "axios": "^0.26.1",
     "clsx": "^1.1.1",
     "final-form": "^4.20.6",
@@ -36,6 +37,7 @@
     "@types/node": "17.0.31",
     "@types/react": "18.0.8",
     "@types/react-dom": "18.0.3",
+    "@types/react-slick": "^0.23.8",
     "@types/validator": "^13.7.2",
     "@typescript-eslint/eslint-plugin": "^5.18.0",
     "@typescript-eslint/parser": "^5.0.0",

+ 3 - 2
packages/dashboard/src/components/Layout/Menu.tsx

@@ -1,5 +1,5 @@
 import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-icons/ai';
-import { FaRegMoon } from 'react-icons/fa';
+import { FaAppStore, 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';
@@ -45,7 +45,8 @@ const SideMenu: React.FC = () => {
       <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('My Apps', 'apps', AiOutlineAppstore)}
+        {renderMenuItem('App Store', 'app-store', FaAppStore)}
         {renderMenuItem('Settings', 'settings', AiOutlineSetting)}
       </List>
       <Divider className="my-3" />

+ 25 - 0
packages/dashboard/src/modules/AppStore/components/FeaturedApps.tsx

@@ -0,0 +1,25 @@
+import { AppConfig } from '@runtipi/common';
+import React from 'react';
+import { Box, Button, Flex } from '@chakra-ui/react';
+import FeaturedCard from './FeaturedCard';
+
+interface IProps {
+  apps: AppConfig[];
+}
+
+const FeaturedApps: React.FC<IProps> = ({ apps }) => {
+  const [appIndex, setAppIndex] = React.useState(0);
+
+  return (
+    <Flex className="flex-col relative">
+      <Box className="relative mb-3" height={200}>
+        {apps.map((app, index) => {
+          return <FeaturedCard show={index === appIndex} key={app.id} app={app} />;
+        })}
+      </Box>
+      <Button onClick={() => setAppIndex(1)}>Next</Button>
+    </Flex>
+  );
+};
+
+export default FeaturedApps;

+ 36 - 0
packages/dashboard/src/modules/AppStore/components/FeaturedCard.tsx

@@ -0,0 +1,36 @@
+import { Flex, ScaleFade } from '@chakra-ui/react';
+import { AppConfig } from '@runtipi/common';
+import React from 'react';
+
+interface IProps {
+  app: AppConfig;
+  show: boolean;
+}
+
+const FeaturedCard: React.FC<IProps> = ({ app, show }) => {
+  return (
+    <ScaleFade initialScale={0.9} in={show}>
+      <Flex
+        className="overflow-hidden absolute left-0 right-0 border-2"
+        height={200}
+        rounded="md"
+        shadow="md"
+        style={{
+          backgroundImage: `url(https://images.unsplash.com/photo-1488590528505-98d2b5aba04b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80)`,
+        }}
+      >
+        <div className="relative flex flex-1 w-max lg:bg-gradient-to-r from-white via-white">
+          <div className="flex absolute bottom-0 flex-row p-3">
+            <img src={app.image} width={80} height={80} className="rounded-lg mr-2" />
+            <div className="self-end mb-1">
+              <div className="font-bold text-xl">{app.name}</div>
+              <div className="text-md">{app.short_desc}</div>
+            </div>
+          </div>
+        </div>
+      </Flex>
+    </ScaleFade>
+  );
+};
+
+export default FeaturedCard;

+ 24 - 0
packages/dashboard/src/modules/AppStore/containers/AppStoreContainer.tsx

@@ -0,0 +1,24 @@
+import { Flex } from '@chakra-ui/react';
+import { AppCategoriesEnum } from '@runtipi/common';
+import React from 'react';
+import { useAppsStore } from '../../../state/appsStore';
+import FeaturedApps from '../components/FeaturedApps';
+
+function nonNullable<T>(value: T): value is NonNullable<T> {
+  return value !== null && value !== undefined;
+}
+
+const AppStoreContainer = () => {
+  const { apps } = useAppsStore();
+
+  const featuredApps = apps.map((app) => (app.categories?.includes(AppCategoriesEnum.FEATURED) ? app : null)).filter(nonNullable);
+
+  return (
+    <Flex className="flex-col">
+      <h1 className="font-bold text-3xl mb-5">App Store</h1>
+      <FeaturedApps apps={featuredApps} />
+    </Flex>
+  );
+};
+
+export default AppStoreContainer;

+ 22 - 0
packages/dashboard/src/pages/app-store/index.tsx

@@ -0,0 +1,22 @@
+import React, { useEffect } from 'react';
+import type { NextPage } from 'next';
+import Layout from '../../components/Layout';
+import AppStoreContainer from '../../modules/AppStore/containers/AppStoreContainer';
+import { useAppsStore } from '../../state/appsStore';
+import { RequestStatus } from '../../core/types';
+
+const Apps: NextPage = () => {
+  const { fetch, status } = useAppsStore((state) => state);
+
+  useEffect(() => {
+    fetch();
+  }, [fetch]);
+
+  return (
+    <Layout loading={status === RequestStatus.LOADING}>
+      <AppStoreContainer />
+    </Layout>
+  );
+};
+
+export default Apps;

+ 4 - 1
packages/system-api/.gitignore

@@ -1,2 +1,5 @@
 node_modules/
-dist/
+dist/
+
+# testing
+/coverage

+ 14 - 4
packages/system-api/Dockerfile

@@ -1,3 +1,15 @@
+### BUILD ###
+FROM node:18-buster-slim AS build
+
+WORKDIR /app
+COPY ./package.json ./
+
+RUN npm install --quiet node-gyp -g
+RUN npm install
+
+RUN npm run build
+
+### MAIN ###
 FROM ubuntu:20.04
 ARG DEBIAN_FRONTEND=noninteractive
 
@@ -33,10 +45,8 @@ RUN chmod +x /usr/local/bin/docker-compose
 
 COPY ./package.json ./
 
-RUN npm install
-
-COPY ./ ./
+RUN npm install --production
 
-RUN npm run build
+COPY --from=build /app/dist /app/dist
 
 CMD ["npm", "run", "start"]

+ 5 - 0
packages/system-api/jest.config.cjs

@@ -1,7 +1,12 @@
 /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
 module.exports = {
   preset: 'ts-jest',
+  verbose: true,
   testEnvironment: 'node',
   testMatch: ['**/__tests__/**/*.test.ts'],
   setupFiles: ['<rootDir>/tests/dotenv-config.ts'],
+  collectCoverage: true,
+  collectCoverageFrom: ['src/**/*.{ts,tsx}'],
+  coverageProvider: 'v8',
+  passWithNoTests: true,
 };

+ 2 - 2
packages/system-api/package.json

@@ -10,7 +10,7 @@
   "scripts": {
     "clean": "rimraf dist",
     "lint": "eslint . --ext .ts",
-    "test": "jest",
+    "test": "jest --colors",
     "test:watch": "jest --watch",
     "build": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --minify --analyze=verbose --external:./node_modules/* --format=esm",
     "build:watch": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --external:./node_modules/* --format=esm --watch",
@@ -21,7 +21,7 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "@runtipi/common": "^0.2.7",
+    "@runtipi/common": "file:../common",
     "argon2": "^0.28.5",
     "axios": "^0.26.1",
     "compression": "^1.7.4",

+ 6 - 2
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -43,14 +43,18 @@ const testApp3: Partial<AppConfig> = {
 
 const MOCK_FILE_EMPTY = {
   [`${config.ROOT_FOLDER}/apps/test-app/config.json`]: JSON.stringify(testApp),
+  [`${config.ROOT_FOLDER}/apps/test-app/metadata/description.md`]: 'md desc',
   [`${config.ROOT_FOLDER}/.env`]: 'TEST=test',
   [`${config.ROOT_FOLDER}/state/apps.json`]: '{"installed": ""}',
 };
 
 const MOCK_FILE_INSTALLED = {
   [`${config.ROOT_FOLDER}/apps/test-app/config.json`]: JSON.stringify(testApp),
+  [`${config.ROOT_FOLDER}/apps/test-app/metadata/description.md`]: 'md desc',
   [`${config.ROOT_FOLDER}/apps/test-app2/config.json`]: JSON.stringify(testApp2),
+  [`${config.ROOT_FOLDER}/apps/test-app2/metadata/description.md`]: 'md desc',
   [`${config.ROOT_FOLDER}/apps/test-app3/config.json`]: JSON.stringify(testApp3),
+  [`${config.ROOT_FOLDER}/apps/test-app3/metadata/description.md`]: 'md desc',
   [`${config.ROOT_FOLDER}/.env`]: 'TEST=test',
   [`${config.ROOT_FOLDER}/state/apps.json`]: '{"installed": "test-app"}',
   [`${config.ROOT_FOLDER}/app-data/test-app`]: '',
@@ -253,8 +257,8 @@ describe('List apps', () => {
     const apps = await AppsService.listApps();
 
     expect(apps).toEqual([
-      { ...testApp, installed: true, status: 'stopped' },
-      { ...testApp2, installed: false, status: 'stopped' },
+      { ...testApp, installed: true, status: 'stopped', description: 'md desc' },
+      { ...testApp2, installed: false, status: 'stopped', description: 'md desc' },
     ]);
     expect(apps.length).toBe(2);
     expect(apps[0].id).toBe('test-app');

+ 2 - 2
packages/system-api/src/modules/apps/apps.service.ts

@@ -48,7 +48,7 @@ const listApps = async (): Promise<AppConfig[]> => {
     .map((app) => {
       try {
         return readJsonFile(`/apps/${app}/config.json`);
-      } catch {
+      } catch (e) {
         return null;
       }
     })
@@ -62,7 +62,7 @@ const listApps = async (): Promise<AppConfig[]> => {
   apps.forEach((app) => {
     app.installed = installed.includes(app.id);
     app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
-    app.description = readFile(`/apps/${app.id}/description.md`);
+    app.description = readFile(`/apps/${app.id}/metadata/description.md`);
   });
 
   return apps;

+ 2 - 2
packages/system-api/src/modules/auth/__tests__/auth.controller.test.ts

@@ -94,7 +94,7 @@ describe('Me', () => {
   it('Should return user if present in request', async () => {
     const json = jest.fn();
     const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
-    const req = { user } as Request;
+    const req = { user } as unknown as Request;
 
     await AuthController.me(req, res, next);
 
@@ -137,7 +137,7 @@ describe('isConfigured', () => {
 
     const json = jest.fn();
     const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
-    const req = { user } as Request;
+    const req = { user } as unknown as Request;
 
     await AuthController.isConfigured(req, res, next);
 

+ 0 - 4
packages/system-api/src/modules/auth/auth.service.ts

@@ -26,10 +26,6 @@ const register = async (email: string, password: string, name: string) => {
     throw new Error('Missing email or password');
   }
 
-  if (users.find((user) => user.email === email)) {
-    throw new Error('User already exists');
-  }
-
   const hash = await argon2.hash(password);
   const newuser: IUser = { email, name, password: hash };
 

+ 31 - 5
pnpm-lock.yaml

@@ -37,11 +37,12 @@ importers:
       '@emotion/react': ^11
       '@emotion/styled': ^11
       '@fontsource/open-sans': ^4.5.8
-      '@runtipi/common': ^0.2.1
+      '@runtipi/common': file:../common
       '@types/js-cookie': ^3.0.2
       '@types/node': 17.0.31
       '@types/react': 18.0.8
       '@types/react-dom': 18.0.3
+      '@types/react-slick': ^0.23.8
       '@types/validator': ^13.7.2
       '@typescript-eslint/eslint-plugin': ^5.18.0
       '@typescript-eslint/parser': ^5.0.0
@@ -59,6 +60,7 @@ importers:
       next: 12.1.6
       postcss: ^8.4.12
       react: 18.1.0
+      react-awesome-slider: ^4.1.0
       react-dom: 18.1.0
       react-final-form: ^6.5.9
       react-icons: ^4.3.1
@@ -73,7 +75,7 @@ importers:
       '@emotion/react': 11.9.0_4mdsreeeydipjms3kbrjyybtve
       '@emotion/styled': 11.8.1_tnefweo2a67ybg6wfzi6ieqilm
       '@fontsource/open-sans': 4.5.8
-      '@runtipi/common': link:../common
+      '@runtipi/common': file:packages/common
       axios: 0.26.1
       clsx: 1.1.1
       final-form: 4.20.7
@@ -82,6 +84,7 @@ importers:
       js-cookie: 3.0.1
       next: 12.1.6_talmm3uuvp6ssixt2qevhfgvue
       react: 18.1.0
+      react-awesome-slider: 4.1.0
       react-dom: 18.1.0_react@18.1.0
       react-final-form: 6.5.9_bnxchjdfy45cdln7bu7hnhf37u
       react-icons: 4.3.1_react@18.1.0
@@ -95,6 +98,7 @@ importers:
       '@types/node': 17.0.31
       '@types/react': 18.0.8
       '@types/react-dom': 18.0.3
+      '@types/react-slick': 0.23.8
       '@types/validator': 13.7.2
       '@typescript-eslint/eslint-plugin': 5.22.0_oztpoyrbzkyaikrhdkppp3gagu
       '@typescript-eslint/parser': 5.22.0_uhoeudlwl7kc47h4kncsfowede
@@ -109,7 +113,7 @@ importers:
 
   packages/system-api:
     specifiers:
-      '@runtipi/common': ^0.2.2
+      '@runtipi/common': file:../common
       '@types/compression': ^1.7.2
       '@types/cookie-parser': ^1.4.3
       '@types/cors': ^2.8.12
@@ -157,7 +161,7 @@ importers:
       ts-jest: ^28.0.2
       typescript: 4.6.4
     dependencies:
-      '@runtipi/common': link:../common
+      '@runtipi/common': file:packages/common
       argon2: 0.28.5
       axios: 0.26.1
       compression: 1.7.4
@@ -2243,6 +2247,12 @@ packages:
       '@types/react': 18.0.8
     dev: true
 
+  /@types/react-slick/0.23.8:
+    resolution: {integrity: sha512-SfzSg++/3uyftVZaCgHpW+2fnJFsyJEQ/YdsuqfOWQ5lqUYV/gY/UwAnkw4qksCj5jalto/T5rKXJ8zeFldQeA==}
+    dependencies:
+      '@types/react': 18.0.8
+    dev: true
+
   /@types/react/18.0.8:
     resolution: {integrity: sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==}
     dependencies:
@@ -4045,7 +4055,7 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
+      '@typescript-eslint/parser': 5.22.0_uhoeudlwl7kc47h4kncsfowede
       debug: 3.2.7
       eslint-import-resolver-node: 0.3.6
       find-up: 2.1.0
@@ -6806,6 +6816,12 @@ packages:
       strip-json-comments: 2.0.1
     dev: true
 
+  /react-awesome-slider/4.1.0:
+    resolution: {integrity: sha512-cbPI1MTpVLKbEH6gf9bwtJb8Ja6R/YJonKbUQehfq2B2AAJkgDMeHntaa4SgGCRqWd55xKiT+CkjfKau1QRsKw==}
+    dependencies:
+      web-animation-club: 0.6.0
+    dev: false
+
   /react-clientside-effect/1.2.6_react@18.1.0:
     resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==}
     peerDependencies:
@@ -7759,6 +7775,10 @@ packages:
       makeerror: 1.0.12
     dev: true
 
+  /web-animation-club/0.6.0:
+    resolution: {integrity: sha512-9W+EQu1HiaPLe/7WZlhJ2ULqQ4VL80RPDYW+ZcjfTKp6ayOuT1k3SVO6+tu0VBRmOqueJ/mrG+rjjInIv8Aglg==}
+    dev: false
+
   /webidl-conversions/3.0.1:
     resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=}
     dev: false
@@ -7903,3 +7923,9 @@ packages:
     dependencies:
       react: 18.1.0
     dev: false
+
+  file:packages/common:
+    resolution: {directory: packages/common, type: directory}
+    name: '@runtipi/common'
+    version: 0.2.8
+    dev: false