diff --git a/.dockerignore b/.dockerignore index 0fc6ac8..132a131 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,6 +7,8 @@ tests .github .env public/s +backend/public/s +frontend public-docker temp tmp @@ -14,4 +16,5 @@ tmp settings.json docker/runtime docker/log +old archiv \ No newline at end of file diff --git a/.github/workflows/cypresss.yml b/.github/workflows/cypresss.yml new file mode 100644 index 0000000..85bf0b9 --- /dev/null +++ b/.github/workflows/cypresss.yml @@ -0,0 +1,24 @@ +name: End-to-end tests +on: + push: + branches: [ "refactor/separate-backend" ] + workflow_dispatch: +jobs: + cypress-run: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run backend + uses: actions/setup-node@v4 + with: + node-version: 18 + - run: | + yarn && + yarn build && + chmod a+x startbe.sh && + ./startbe.sh + - name: Run E2E tests + uses: cypress-io/github-action@v6 + with: + start: yarn preview \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0cfe791..e1bb37f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,41 @@ -.idea/ -.vscode/ -node_modules/ -build/ -tmp/ -temp/ -.tmp/ -.nuxt -dist +# Nuxt dev/build outputs .output -*.log* +.data +.nuxt .nitro .cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files .env -*.lock -upload/ -coverage -secret +.env.* +!.env.example + +cypress/e2e/screenshots +cypress/e2e/logs +cypress/screenshots +cypress/logs + public/s -public-docker -org.* -settings.json -docker-compose.yml -docker/runtime -archiv +backend/node_modules +backend/public/s +backend/dist +backend/data/*.sqlite +backend/yarn.lock +backend/.env + +frontend + +yarn.lock diff --git a/.kanbn/index.md b/.kanbn/index.md deleted file mode 100644 index efcea39..0000000 --- a/.kanbn/index.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -startedColumns: - - 'In Progress Mon-Mon' -completedColumns: - - Done -sprints: - - - start: 2022-05-09T07:26:47.305Z - name: 'Fish Create Podcast' ---- - -# Podkashde - -## Backlog - -- [podcast-slug-uniqueness-prüfen](tasks/podcast-slug-uniqueness-prüfen.md) -- [move-episode-to-other-podcast](tasks/move-episode-to-other-podcast.md) -- [uat-test-suite](tasks/uat-test-suite.md) -- [autor-aus-enum-liste-oder-eingeben](tasks/autor-aus-enum-liste-oder-eingeben.md) -- [drag-n-drop-image](tasks/drag-n-drop-image.md) - -## Todo - -- [generate-short-info-from-author-video-and-reference](tasks/generate-short-info-from-author-video-and-reference.md) - -## In Progress - -## Done - -- [login-passwort-change](tasks/login-passwort-change.md) -- [injecting-menu-for-integration-in-website](tasks/injecting-menu-for-integration-in-website.md) -- [analytics](tasks/analytics.md) -- [refresh-bug](tasks/refresh-bug.md) -- [admin-als-super-admin-delete-podcast-und-import](tasks/admin-als-super-admin-delete-podcast-und-import.md) -- [bibelstelle-video-editierbar](tasks/bibelstelle-video-editierbar.md) diff --git a/.kanbn/tasks/admin-als-super-admin-delete-podcast-und-import.md b/.kanbn/tasks/admin-als-super-admin-delete-podcast-und-import.md deleted file mode 100644 index 9108985..0000000 --- a/.kanbn/tasks/admin-als-super-admin-delete-podcast-und-import.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -created: 2022-12-02T17:11:19.646Z -updated: 2022-12-03T14:54:54.320Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-12-03T14:54:54.320Z ---- - -# admin als super admin - Delete Podcast und Import diff --git a/.kanbn/tasks/analytics.md b/.kanbn/tasks/analytics.md deleted file mode 100644 index 86cfcde..0000000 --- a/.kanbn/tasks/analytics.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -created: 2022-11-29T10:53:22.296Z -updated: 2022-12-12T09:35:30.491Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-12-12T09:35:30.491Z ---- - -# Analytics - -https://github.com/ijkml/nuxt-umami - -https://umami.is/docs/collect-data diff --git a/.kanbn/tasks/autor-aus-enum-liste-oder-eingeben.md b/.kanbn/tasks/autor-aus-enum-liste-oder-eingeben.md deleted file mode 100644 index 21ea6f3..0000000 --- a/.kanbn/tasks/autor-aus-enum-liste-oder-eingeben.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -created: 2022-11-10T09:58:36.908Z -updated: 2022-11-10T09:58:44.641Z -assigned: "" -progress: 0 -tags: [] ---- - -# Autor aus Enum Liste oder eingeben diff --git a/.kanbn/tasks/bibelstelle-video-editierbar.md b/.kanbn/tasks/bibelstelle-video-editierbar.md deleted file mode 100644 index 482ea98..0000000 --- a/.kanbn/tasks/bibelstelle-video-editierbar.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -created: 2022-12-02T17:13:07.640Z -updated: 2022-12-03T14:45:20.810Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-12-03T14:45:20.810Z ---- - -# Bibelstelle + Video editierbar diff --git a/.kanbn/tasks/drag-n-drop-image.md b/.kanbn/tasks/drag-n-drop-image.md deleted file mode 100644 index ab20bf7..0000000 --- a/.kanbn/tasks/drag-n-drop-image.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -created: 2022-05-10T07:13:03.869Z -updated: 2022-05-10T07:13:03.867Z -assigned: "" -progress: 0 -tags: [] ---- - -# Drag'n Drop image diff --git a/.kanbn/tasks/generate-short-info-from-author-video-and-reference.md b/.kanbn/tasks/generate-short-info-from-author-video-and-reference.md deleted file mode 100644 index 7e6fbf3..0000000 --- a/.kanbn/tasks/generate-short-info-from-author-video-and-reference.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -created: 2022-12-27T18:37:56.830Z -updated: 2022-12-27T18:40:22.475Z -assigned: "" -progress: 0 -tags: [] ---- - -# Generate ShortInfo from Author, Video and Reference diff --git a/.kanbn/tasks/injecting-menu-for-integration-in-website.md b/.kanbn/tasks/injecting-menu-for-integration-in-website.md deleted file mode 100644 index 1d662e2..0000000 --- a/.kanbn/tasks/injecting-menu-for-integration-in-website.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -created: 2022-12-12T09:35:44.246Z -updated: 2022-12-27T18:19:06.034Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-12-27T18:19:06.035Z ---- - -# Injecting Menu for integration in website diff --git a/.kanbn/tasks/login-passwort-change.md b/.kanbn/tasks/login-passwort-change.md deleted file mode 100644 index 9e6517c..0000000 --- a/.kanbn/tasks/login-passwort-change.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -created: 2022-12-02T17:11:51.309Z -updated: 2022-12-03T14:36:39.641Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-12-03T14:36:39.641Z ---- - -# Login / Passwort change diff --git a/.kanbn/tasks/move-episode-to-other-podcast.md b/.kanbn/tasks/move-episode-to-other-podcast.md deleted file mode 100644 index afab358..0000000 --- a/.kanbn/tasks/move-episode-to-other-podcast.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -created: 2022-12-01T13:11:12.280Z -updated: 2022-12-03T10:33:33.285Z -assigned: "" -progress: 0 -tags: [] ---- - -# Move Episode to other Podcast diff --git a/.kanbn/tasks/paging-for-episodes-client-server.md b/.kanbn/tasks/paging-for-episodes-client-server.md deleted file mode 100644 index 77137f2..0000000 --- a/.kanbn/tasks/paging-for-episodes-client-server.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -created: 2022-11-26T06:53:28.840Z -updated: 2022-11-28T22:36:26.008Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-11-28T22:36:26.008Z ---- - -# Paging for episodes (client/server) - -Ting wiht composables lets me think it should be client for now diff --git "a/.kanbn/tasks/podcast-slug-uniqueness-pr\303\274fen.md" "b/.kanbn/tasks/podcast-slug-uniqueness-pr\303\274fen.md" deleted file mode 100644 index 9295008..0000000 --- "a/.kanbn/tasks/podcast-slug-uniqueness-pr\303\274fen.md" +++ /dev/null @@ -1,9 +0,0 @@ ---- -created: 2022-11-29T07:44:34.498Z -updated: 2022-12-27T18:19:14.357Z -assigned: "" -progress: 0 -tags: [] ---- - -# Podcast Slug uniqueness prüfen diff --git a/.kanbn/tasks/refresh-bug.md b/.kanbn/tasks/refresh-bug.md deleted file mode 100644 index e35f11f..0000000 --- a/.kanbn/tasks/refresh-bug.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -created: 2022-12-03T10:33:07.016Z -updated: 2022-12-03T15:01:26.965Z -assigned: "" -progress: 0 -tags: [] -completed: 2022-12-03T15:01:26.965Z ---- - -# Refresh bug diff --git a/.kanbn/tasks/uat-test-suite.md b/.kanbn/tasks/uat-test-suite.md deleted file mode 100644 index 68e9ec8..0000000 --- a/.kanbn/tasks/uat-test-suite.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -created: 2022-11-10T09:57:43.530Z -updated: 2022-12-03T10:33:35.110Z -assigned: "" -progress: 0 -tags: [] ---- - -# UAT - Test Suite diff --git a/.nuxtignore b/.nuxtignore index 474a3ae..3de839b 100644 --- a/.nuxtignore +++ b/.nuxtignore @@ -8,4 +8,5 @@ tmp temp data docker -archiv +old +archiv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8b29617..673dc25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,59 @@ -FROM node:16 as builder +FROM node:18 LABEL authors="Alex Roehm" -# update dependencies and install curl -ARG BUILDTIME ARG VERSION +ARG BUILDTIME ARG REVISION +ARG NUXT_PUBLIC_URL +ARG NUXT_PUBLIC_UMAMI_HOST +ARG NUXT_PUBLIC_UMAMI_ID +ARG NUXT_PUBLIC_SKIN +ARG NUXT_PUBLIC_ENABLE_DARK_MODE +ARG NUXT_PUBLIC_LOGO +ARG NUXT_PUBLIC_LOGO_DARK +ARG NUXT_PUBLIC_EXT_MENU_DE -# Create app directory -WORKDIR /build -COPY . . -RUN echo export const VERSION=\"${VERSION}\" > version.ts -RUN echo export const BUILDTIME=\"${BUILDTIME}\" >> version.ts -RUN echo export const REVISION=\"${REVISION}\" >> version.ts - -# update each dependency in package.json to the latest version -RUN yarn +ENV DATABASE_PATH=/data +ENV DATABASE_FILE=podcasts.sqlite +ENV DATA_PATH=/var/www -# If you are building your code for production -RUN yarn build - -FROM node:16 -LABEL authors="Alex Roehm" RUN apt-get update && apt-get install -y \ - curl dumb-init logrotate nginx\ + curl dumb-init logrotate nginx vim \ && rm -rf /var/lib/apt/lists/* -COPY ./docker/nginx/startup.sh /startup.sh -RUN chmod u+x /startup.sh - +# prepare nginx COPY ./docker/nginx/default /etc/nginx/sites-available/default COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf COPY ./docker/nginx/logrotate /etc/logrotate/nginx -COPY --from=builder --chown=node:node /build/.output /var/www +RUN chown -R node:node /etc/nginx /var/log/nginx /var/lib/nginx RUN chmod go-w /etc/logrotate/nginx -RUN chown -R node:node /var/www /etc/nginx /var/log/nginx /var/lib/nginx RUN chmod a+w /run -WORKDIR /var/www +WORKDIR /data +RUN chown node:node . + +WORKDIR /app +RUN chown node:node . +COPY --chown=node:node . . USER node +# install startup script +RUN cp docker/nginx/startup.sh . +RUN chmod a+x startup.sh + +# prepare backend +RUN cd ./backend && yarn + +# Prepare Versioning +RUN printf 'export const BUILDTIME= "'$BUILDTIME'"\n' > version.ts +RUN printf 'export const REVISION= "'$REVISION'"\n' >> version.ts +RUN printf 'export const VERSION = "'$VERSION'"\n' >> version.ts + +# update each dependency in package.json to the latest version +RUN yarn + EXPOSE 80 ENTRYPOINT ["/usr/bin/dumb-init", "--"] -CMD [ "/startup.sh" ] \ No newline at end of file +CMD [ "./startup.sh" ] \ No newline at end of file diff --git a/README.md b/README.md index 8cbdc64..f5db2a2 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,75 @@ -# Podkashde +# Nuxt 3 Minimal Starter -Podkashde is a jamstack podcast management system. It is based on [nuxt 3](https://v3.nuxtjs.org). It is suposed to be a replacement for a podcast hostet on wordpress with seriously simple podcast and allows to migrate the podsts and metadata. +Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. -It uses a sqlite database. That way backup is easy, just backup the files in the directory public/s -All assets (mp3s, cover images) can be served statically from the public/s directoy +## Setup -## Development - -You need node version 16. -Clone the repo. Install the dependencies: +Make sure to install the dependencies: ```bash -# yarn -yarn install - # npm npm install # pnpm -pnpm install --shamefully-hoist +pnpm install + +# yarn +yarn install + +# bun +bun install ``` -Start the development server on http://localhost:3000 +## Development Server + +Start the development server on `http://localhost:3000`: ```bash +# npm npm run dev + +# pnpm +pnpm run dev + +# yarn +yarn dev + +# bun +bun run dev ``` -## Standalone Production +## Production Build the application for production: ```bash +# npm npm run build -``` \ No newline at end of file + +# pnpm +pnpm run build + +# yarn +yarn build + +# bun +bun run build +``` + +Locally preview production build: + +```bash +# npm +npm run preview + +# pnpm +pnpm run preview + +# yarn +yarn preview + +# bun +bun run preview +``` + +Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/app.vue b/app.vue index c887fb3..2bcbca5 100644 --- a/app.vue +++ b/app.vue @@ -1,74 +1,11 @@ - + \ No newline at end of file diff --git a/assets/css/tailwind.css b/assets/css/tailwind.css index 656199d..9a43deb 100644 --- a/assets/css/tailwind.css +++ b/assets/css/tailwind.css @@ -9,7 +9,7 @@ --color-text-muted: 80, 80, 80; --color-text-muted-dark: 175, 155, 175; --color-text-inverted: 235, 250, 204; - --color-text-accent: 25, 0, 24; + --color-text-accent: 99, 102, 241; --color-bg-light: 255, 245, 255; --color-bg-dark: 10, 3, 13; --color-bg-muted: 220, 215, 235; @@ -338,4 +338,19 @@ .progressbar::-moz-range-progress { height: 4px; background: rgb(216, 216, 216); +} + +.page-enter-active, +.page-enter-leave { + transition: all 0.5s; +} + +.page-enter-from { + opacity: 0; + transform: translateY(100%) scale(0.9); +} + +.page-leave-to { + opacity: 0; + transform: translateY(-100%) scale(0.9); } \ No newline at end of file diff --git a/assets/favicon.ico b/assets/favicon.ico deleted file mode 100644 index e08b257..0000000 Binary files a/assets/favicon.ico and /dev/null differ diff --git a/public/img/ccf-logo-w.png b/assets/img/ccf-logo-w.png similarity index 100% rename from public/img/ccf-logo-w.png rename to assets/img/ccf-logo-w.png diff --git a/public/img/ccf-logo.png b/assets/img/ccf-logo.png similarity index 100% rename from public/img/ccf-logo.png rename to assets/img/ccf-logo.png diff --git a/public/img/logo-w.png b/assets/img/logo-w.png similarity index 100% rename from public/img/logo-w.png rename to assets/img/logo-w.png diff --git a/public/img/logo.png b/assets/img/logo.png similarity index 100% rename from public/img/logo.png rename to assets/img/logo.png diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0efe086 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,13 @@ +# Example how to use Express and TypeORM with TypeScript + +1. clone repository +2. run `npm i` +3. edit `ormconfig.json` and change your database configuration (you can also change a database type, but don't forget to install specific database drivers) +4. run `npm start` +5. open `http://localhost:3000/posts` and you'll empty array +6. use curl, postman or other tools to send http requests to test your typeorm-based API + +## How to use CLI? + +1. install `typeorm` globally: `npm i -g typeorm` +2. run `typeorm -h` to show list of available commands \ No newline at end of file diff --git a/backend/data/files/menu-ext-de.json b/backend/data/files/menu-ext-de.json new file mode 100644 index 0000000..d148975 --- /dev/null +++ b/backend/data/files/menu-ext-de.json @@ -0,0 +1,66 @@ +{ + "menu": [ + { + "Description": "Höre unsere Predigen, Inputs und Biblestudies oder lies Neues und Andachten", + "Name": "Hör rein", + "Replaces": "Hör rein", + "link": "/recent", + "menu_items": [ + { + "Name": "Aktuelle Predigten", + "locale": "de", + "link": "/recent", + "local": true + }, + { + "Name": "Aktuelle Serien", + "locale": "de", + "link": "/series", + "local": true + }, + { + "Name": "Podcasts", + "locale": "de", + "link": "/podcasts", + "local": true + } + ] + }, + { + "Description": "Podcasts, Serien und Folgen verwalten ", + "Name": "Administrator", + "admin": true, + "link": "/admin", + "menu_items": [ + { + "Name": "Andministrator Login", + "locale": "de", + "loggedin": false, + "link": "/admin/login", + "local": true + }, + { + "Name": "Passwort ändern", + "loggedin": true, + "locale": "de", + "link": "/admin/setpassword", + "local": true + }, + { + "Name": "Benutzer einladen", + "locale": "de", + "loggedin": true, + "link": "/admin/invitation", + "local": true + }, + { + "Name": "Abmelden", + "locale": "de", + "loggedin": true, + "link": "#logout", + "local": true + } + ] + } + ] +} \ No newline at end of file diff --git a/backend/data/files/meta-de.json b/backend/data/files/meta-de.json new file mode 100644 index 0000000..c6d0222 --- /dev/null +++ b/backend/data/files/meta-de.json @@ -0,0 +1,65 @@ +{ + "menu": [{ + "Description": "Höre aktuelle und zuletzt erschienene Folgen", + "Name": "Hör rein", + "Replaces": "Hör rein", + "link": "/recent", + "menu_items": [ + { + "Name": "Aktuelle Folgen", + "locale": "de", + "link": "/recent", + "local": true + }, + { + "Name": "Aktuelle Serien", + "locale": "de", + "link": "/series", + "local": true + }, + { + "Name": "Podcasts", + "locale": "de", + "link": "/podcasts", + "local": true + } + ] + }, + { + "Description": "Podcasts, Serien und Folgen verwalten ", + "Name": "Administrator", + "admin": true, + "link": "/admin", + "menu_items": [ + { + "Name": "Andministrator Login", + "locale": "de", + "loggedin": false, + "link": "/admin/login", + "local": true + }, + { + "Name": "Passwort ändern", + "loggedin": true, + "locale": "de", + "link": "/admin/setpassword", + "local": true + }, + { + "Name": "Benutzer einladen", + "locale": "de", + "loggedin": true, + "link": "/admin/invitation", + "local": true + }, + { + "Name": "Abmelden", + "locale": "de", + "loggedin": true, + "link": "#logout", + "local": true + } + ] + } + ] +} \ No newline at end of file diff --git a/backend/data/files/meta-en.json b/backend/data/files/meta-en.json new file mode 100644 index 0000000..fa24ad7 --- /dev/null +++ b/backend/data/files/meta-en.json @@ -0,0 +1,66 @@ +{ + "menu": [ + { + "Description": "Listen to our content", + "Name": "Listen", + "Replaces": "Listen", + "link": "/recent", + "menu_items": [ + { + "Name": "Recent Episodes", + "locale": "en", + "link": "/recent", + "local": true + }, + { + "Name": "Current Series", + "locale": "en", + "link": "/series", + "local": true + }, + { + "Name": "Podcasts", + "locale": "en", + "link": "/podcasts", + "local": true + } + ] + }, + { + "Description": "Manage podcasts, series and episodes", + "Name": "Administration", + "admin": true, + "link": "/admin", + "menu_items": [ + { + "Name": "Andmin Login", + "locale": "en", + "loggedin": false, + "link": "/admin/login", + "local": true + }, + { + "Name": "Change Password", + "loggedin": true, + "locale": "en", + "link": "/admin/setpassword", + "local": true + }, + { + "Name": "Invite User", + "locale": "en", + "loggedin": true, + "link": "/admin/invitation", + "local": true + }, + { + "Name": "Logout", + "locale": "en", + "loggedin": true, + "link": "#logout", + "local": true + } + ] + } + ] +} \ No newline at end of file diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000..85c664c --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + transform: {'^.+\\.ts?$': 'ts-jest'}, + testEnvironment: 'node', + testRegex: '/tests/.*\\.(test|spec)?\\.(ts|tsx)$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] + }; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..7d8b59e --- /dev/null +++ b/backend/package.json @@ -0,0 +1,47 @@ +{ + "name": "pk-backend", + "version": "0.0.1", + "description": "Backend for CCF Podkashde Podkast tool", + "license": "MIT", + "readmeFilename": "README.md", + "author": { + "name": "Alex Roehm" + }, + "repository": { + "type": "git" + }, + "devDependencies": { + "@types/body-parser": "^1.19.2", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.11", + "@types/node": "^18.15.11", + "jest": "^29.7.0", + "mocha": "^10.2.0", + "nodemon": "^3.0.3", + "typescript": "^5.0.4" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "body-parser": "^1.20.2", + "busboy": "^1.6.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-http-proxy": "^2.0.0", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^2.6.6", + "node-id3": "^0.2.6", + "podcast": "^2.0.1", + "sqlite3": "^5.0.2", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "typeorm": "^0.3.14" + }, + "scripts": { + "start": "ts-node src/index.ts", + "typeorm": "typeorm-ts-node-commonjs", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts", + "test": "jest", + "testcont": "npx jest --watch" + } +} diff --git a/backend/public/img/ccf-logo-w.png b/backend/public/img/ccf-logo-w.png new file mode 100644 index 0000000..3929c2e Binary files /dev/null and b/backend/public/img/ccf-logo-w.png differ diff --git a/backend/public/img/ccf-logo.png b/backend/public/img/ccf-logo.png new file mode 100644 index 0000000..273946d Binary files /dev/null and b/backend/public/img/ccf-logo.png differ diff --git a/backend/public/img/logo-w.png b/backend/public/img/logo-w.png new file mode 100644 index 0000000..e0c7cdd Binary files /dev/null and b/backend/public/img/logo-w.png differ diff --git a/backend/public/img/logo.png b/backend/public/img/logo.png new file mode 100644 index 0000000..b7486c1 Binary files /dev/null and b/backend/public/img/logo.png differ diff --git a/backend/public/test/img/cover.jpg b/backend/public/test/img/cover.jpg new file mode 100644 index 0000000..78063ed Binary files /dev/null and b/backend/public/test/img/cover.jpg differ diff --git a/backend/src/authMiddleware.ts b/backend/src/authMiddleware.ts new file mode 100644 index 0000000..b4b3ba3 --- /dev/null +++ b/backend/src/authMiddleware.ts @@ -0,0 +1,75 @@ +import { Express, Request, Response, NextFunction } from "express"; +import { + decodeAccessToken, + decodeRefreshToken, + decodeUrlToken, + generateAccessToken, + sendAuthToken, +} from "./jwt"; +import { getUserById } from "./services/userService"; +import { logger } from "./services/loggerService"; +import { respond } from "./tools/Controller"; + +export default async function authMiddleware(req: Request, res: Response, next: NextFunction) { + const dbg = 4 + var token = "" + if (req.headers.authorization && req.headers.authorization.startsWith("Bearer ") && req.headers.authorization.length > 10 ) { + logger( dbg, "Auth Middleware authorization " + JSON.stringify(req.headers.authorization).substring(0,30)); + token = req.headers["authorization"]?.split(" ")[1]; + logger( dbg, "Auth Middleware token " + JSON.stringify(token).substring(0,50) + ' ...') + } + var decoded = decodeAccessToken(token); + logger( dbg, "Auth Middleware decoded token " + JSON.stringify(decoded?decoded:"decoding failed!").substring(0,30)); + if (!decoded && req.cookies) { + logger( dbg, "Auth Middleware cookies " + JSON.stringify(req.cookies).substring(0,50)) + token = req.cookies?.auth_token + if (token) + decoded = decodeAccessToken(token); + logger( dbg, "Auth Middleware auth cookie decoded " + JSON.stringify(decoded?decoded:"decoding failed").substring(0,50)); + } + if (!decoded && req.body) { + token = req.body?.refresh_token + if (token) + decoded = decodeRefreshToken(token); + logger( dbg, "Auth Middleware refresh token decoded " + JSON.stringify(decoded?decoded:"decoding failed").substring(0,50)); + } + if (!decoded && req.query ) { + logger( dbg, "Auth Middleware params " + JSON.stringify(req.query).substring(0,50)); + token = req.query.token as string + if (token) + decoded = decodeUrlToken(token); + } + if (!decoded && req.body ) { + logger( dbg, "Auth Middleware params " + JSON.stringify(req.body).substring(0,50)); + token = req.body.token as string + if (token) + decoded = decodeUrlToken(token); + } + if (!decoded) { + respond(res, 401, { + statusCode: 401, + message: "Unauthorized" + }) + } else + try { + if (decoded.userId) { + logger( dbg, "Auth Middleware try access " + JSON.stringify(decoded).substring(0,50)); + + const userId = decoded.userId; + logger( dbg, "Auth Middleware try get UserId " + JSON.stringify(userId).substring(0,30)); + + const user = await getUserById(userId); + logger( dbg, "Auth Middleware user " + JSON.stringify(user).substring(0,50)); + + sendAuthToken(res, await generateAccessToken(user)); + } + logger(4, "Auth... going on") + next() + return; + } catch (error) { + return respond(res, 401, { + statusCode: 401, + statusMessage: error.message, + }); + } +} diff --git a/backend/src/controller/All.ts b/backend/src/controller/All.ts new file mode 100644 index 0000000..0797dbf --- /dev/null +++ b/backend/src/controller/All.ts @@ -0,0 +1,86 @@ +import {Request, Response} from "express"; +import { deleteGen, deleteIdGen, getAllGen, getExtManyGen, getExtQueryAllGen, getExtQueryGen, isUpdate, saveGen, updateGen } from "../services/genericService"; +import { respond, sendResponse } from "../tools/Controller"; + +/** + * Loads all posts from the database. + */ + +export async function getExtMany(T: any, relations: Array, request: Request, response: Response) { + var tmpQuery = { + where: request.query, + relations: relations, + }; + sendResponse( response, await getExtManyGen(T, tmpQuery)); +} + +export async function getAll(T: any, request: Request, response: Response) { + sendResponse( response, await getAllGen(T)); +} + +export async function getExtQuery(T: any, extQuery: any, request: Request, response: Response) { + sendResponse( response, await getExtQueryGen(T, extQuery)); +} + +export async function getExtQueryAll(T: any, extQuery: any, request: Request, response: Response) { + sendResponse( response, await getExtQueryAllGen(T, extQuery)); +} + + +export async function getQuery(T: any, relations: Array, request: Request, response: Response) { + + var tmpQuery = { + where: request.query, + relations: relations, + }; + sendResponse( response, await getExtQueryGen(T, tmpQuery)); +} + +export async function deleteQuery(T: any,request: Request, response: Response) { + + if (await deleteGen(T, request.body)) + respond(response, 201, {statusCode: 201, message: JSON.stringify(request.body) +" deleted successfully"}) + else + respond(response, 205, {statusCode: 205, message: "Delete did not work"}); +} + +export async function saveOne(T: any, data: any ) : Promise { + var id: number | undefined = undefined + if( isUpdate(data) ) + id = await updateGen(T, data) + else + id = await saveGen(T, data) + return id +} + +export async function simpleSave(T: any, request: Request, response: Response) { + try { + var data = {... request.body} as T; + const id = await saveOne(T,data) + if (id) + respond(response, 201, {statusCode: 201, message: "id="+id+" saved successfully", id}) + else + respond(response, 500, {statusCode: 500, message: "id="+id+" problem", id}) + } catch(err) { + respond(response, 500, {statusCode: 500, message: err.message}); + } +} + +export async function simpleSaveMultiple(T: any, request: Request, response: Response) { + try { + var data = request.body as Array; + const len = data.length + var ids = new Array() + for await (const item of data) { + const id = await saveOne(T,item) + if (id) + ids.push(id) + } + if (ids.length==len) + respond(response, 201, {statusCode: 201, message: JSON.stringify(ids) + " ids saved successfully"}) + else + respond(response, 500, {statusCode: 500, message: "Not all ids saved"}) + } catch(err) { + respond(response, 500, {statusCode: 500, message: err.message}); + } +} \ No newline at end of file diff --git a/backend/src/controller/Auth.ts b/backend/src/controller/Auth.ts new file mode 100644 index 0000000..9088455 --- /dev/null +++ b/backend/src/controller/Auth.ts @@ -0,0 +1,203 @@ +import { Request, Response } from "express"; +import * as bcrypt from "bcrypt"; +import { + createUserWithToken, + getUserById, + getUserByUserName, + sanitizeUserForFrontend, + updateUser, +} from "../services/userService"; +import { + decodeRefreshToken, + decodeUrlToken, + deleteRefreshToken, + generateAccessToken, + generateRefreshToken, + sendAuthToken, + sendRefreshToken, +} from "../jwt"; +import { createSession, getSessionByToken, removeOldSessions, removeSession } from "../services/sessionService"; +import User from "../entities/User"; +import { respond, sendResponse } from "../tools/Controller"; + +export async function getUserToken(request: Request, response: Response) { + const user = await createUserWithToken({ username: request.query.username } as User, request.query.type as string); + sendResponse( response, user.token) +} + +export async function checkRefreshToken(request: Request, response: Response) { + const refreshToken = request.body.refresh_token; + if (!refreshToken) + return respond(response, 400, { + statusCode: 400, + statusMessage: "No Refresh token", + }); + + const session = await getSessionByToken(refreshToken); + + if (!session) { + return respond(response, 400, { + statusCode: 400, + statusMessage: "Refresh token is invalid", + }); + } + + const token = decodeRefreshToken(refreshToken); + if (!token) { + const days = 14; + const priorByDays = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + await removeOldSessions(priorByDays); + deleteRefreshToken(response); + + return respond(response, 400, { + statusCode: 403, + statusMessage: "Session expired", + }); + } + + try { + const accessToken = generateAccessToken(token.userId); + const user = sanitizeUserForFrontend(await getUserById(token.userId)); + user.token = accessToken; + sendResponse( response, { refresh_token: refreshToken, access_token: accessToken, user }); + } catch (error) { + return respond(response, 400, { + statusCode: 500, + statusMessage: "Something went wrong", + }); + } +} + +export async function setPassword(request: Request, response: Response) { + var username: string = request.body.username; + const password: string = request.body.password; + const oldpassword: string = request.body.oldpassword; + const token: string = request.body.token; + var credentialValid = false; + + if (!((token && password) || (password && username && oldpassword))) { + return respond(response, 400, { + statusCode: 400, + statusMessage: "Ivalid params", + }); + } + + if (token) { + const data = decodeUrlToken(token); + username = data?.username; + credentialValid = true; + } + + const user = await getUserByUserName(username); + if (!user) { + return respond(response, 400, { + statusCode: 400, + statusMessage: "Username is invalid", + }); + } + + if (!credentialValid && oldpassword) + credentialValid = await bcrypt.compare(oldpassword, user.password); + + if (!credentialValid) { + return respond(response, 400, { + statusCode: 400, + statusMessage: "Username or password is invalid", + }); + } + + const salt = bcrypt.genSaltSync(5); + user.password = bcrypt.hashSync(password, salt); + await updateUser(user); + + const accessToken = generateAccessToken(user); + const refreshToken = generateRefreshToken(user); + await createSession(refreshToken, user.id); + sendRefreshToken(response, refreshToken); + + sendResponse( response, { + access_token: accessToken, + refresh_token: refreshToken, + user: sanitizeUserForFrontend(user), + }); +} + +export async function checkToken(request: Request, response: Response) { + const token = request.query.token as string; + + if (!token) { + return respond(response, 400, { + statusCode: 400, + statusMessage: "Ivalid params", + }); + } + + const data = decodeUrlToken(token); + const user = await getUserByUserName(data?.username); + + if (!user) { + return respond(response, 400, { + statusCode: 400, + statusMessage: "Username or password is invalid", + }); + } + + return sendResponse( response, { + access: data.purpose, + user: sanitizeUserForFrontend(user), + }); +} + +export async function login(request: Request, response: Response) : Promise { + const username: string = request.body.username; + const password: string = request.body.password; + + if (!username || !password) { + respond(response, 400, { + statusCode: 400, + statusMessage: "Ivalid params", + }); + return false + } + + const user = await getUserByUserName(username); + + if (!user) { + respond(response, 400, { + statusCode: 400, + statusMessage: "Username or password is invalid", + }); + return false; + } + const doesThePasswordMatch = await bcrypt.compare(password, user.password); + + if (!doesThePasswordMatch) { + respond(response, 400, { + statusCode: 400, + statusMessage: "Username or password is invalid", + }); + return false; + } + + const accessToken = generateAccessToken(user); + user.token = accessToken; + sendAuthToken(response, accessToken); + + const refreshToken = generateRefreshToken(user); + await createSession(refreshToken, user.id); + sendResponse( response, { + access_token: accessToken, + refresh_token: refreshToken, + user: sanitizeUserForFrontend(user), + }); + return true; +} + +export async function logout(request: Request, response: Response) { + try { + const refreshToken = request.body.refresh_token as string; + await removeSession(refreshToken) + } catch (error) { } + + return respond(response, 200,{ message: 'Done' }) +} diff --git a/backend/src/controller/Count.ts b/backend/src/controller/Count.ts new file mode 100644 index 0000000..b071ee6 --- /dev/null +++ b/backend/src/controller/Count.ts @@ -0,0 +1,34 @@ +import {Request, Response} from "express"; +import Episode from "../entities/Episode"; +import Serie from "../entities/Serie"; +import { Not } from "typeorm"; +import { getDbManager } from "../services/datasourceService"; +import { sendResponse } from "../tools/Controller"; +import Podcast from "../entities/Podcast"; + +/** + * Loads all posts from the database. + */ +export async function count(request: Request, response: Response) { + const query = request.query; + var result = 0; + const q = {} + if (query.hasOwnProperty('excludeId')) { + const id = query['excludeId'] + q['id'] = Not(id) + } + if (query.hasOwnProperty('id')) { + q['id'] = query.id + } + if (query.hasOwnProperty('slug')) { + q['slug'] = query.slug + } + const manager = getDbManager() + if (query.hasOwnProperty('serie')) + result = await manager.countBy(Serie, q); + else if (query.hasOwnProperty('podcast')) + result = await manager.countBy(Podcast, q); + else + result = await manager.countBy(Episode, q); + return sendResponse( response, result.toString()); +} \ No newline at end of file diff --git a/backend/src/controller/Enums.ts b/backend/src/controller/Enums.ts new file mode 100644 index 0000000..8b6aa04 --- /dev/null +++ b/backend/src/controller/Enums.ts @@ -0,0 +1,17 @@ +import {Request, Response} from "express"; +import Enumerator from "../entities/Enumerator"; +import { sendResponse } from "../tools/Controller"; +import getRepository from "../services/datasourceService"; + +export async function setEnums(request: Request, response: Response) { + + const repository = getRepository(Enumerator) + + repository.save(request.body.map((item) => item)); + + // load posts + const enums = await repository.find(); + + // return loaded posts + sendResponse( response, { statusCode: 201, message: "enums saved"}); +} diff --git a/backend/src/controller/Episodes.ts b/backend/src/controller/Episodes.ts new file mode 100644 index 0000000..d6881a1 --- /dev/null +++ b/backend/src/controller/Episodes.ts @@ -0,0 +1,25 @@ +import { Request, Response } from "express"; +import { moveEpisode, saveEpisode } from "../services/episodeService"; +import { respond } from "../tools/Controller"; + +export async function saveEpisodeAction(request: Request, response: Response) { + try { + const episode = await saveEpisode(request.body); + respond(response, 201, { statusCode: 201, message: "Episode id=" + episode.id + " saved successfully", id: episode.id }); + } catch (err) { + respond(response, 500, { statusCode: 500, message: err.message }); + } +} + +export async function moveEpisodeAction(request: Request, response: Response) { + try { + const { episode, podcast, serie } = request.body; + const result = await moveEpisode(episode, podcast, serie); + if (!result) respond(response, 500, { message: (result as any).message }); + respond(response, 201, { statusCode: 201, message: "Episode id=" + episode.id + " saved successfully" }); + } catch (err) { + console.log(err) + respond(response, 500, { statusCode: 500, message: err.message }); + } +} + diff --git a/backend/src/controller/Files.ts b/backend/src/controller/Files.ts new file mode 100644 index 0000000..bde1a64 --- /dev/null +++ b/backend/src/controller/Files.ts @@ -0,0 +1,94 @@ +import * as fs from 'fs'; +import { Request, Response } from "express"; +import { getFileFromUrl } from "../services/filesService"; +import { ARCHIV_PATH, DATA_PATH, UPLOAD_TEMP_PATH } from "../tools/Configuration"; +import { getPathForMediaFile } from "../services/episodeService"; +import { copyFile, createDir, findFile, moveFile } from "../tools/DataFiles"; +import busboy = require('busboy'); +import { respond } from '../tools/Controller'; + +export async function fetchFileAction(request: Request, response: Response) { + + const body = request.body as any; + const result = await getFileFromUrl(body.orgurl,body.newpath, body.altpath, body.newfile) + respond(response, result.statusCode, result) +} + +export async function downloadFileAction(request: Request, response: Response) { + var path = '' + if (request.params.path) + path = DATA_PATH + request.params.path; + else if (request.params.name) { + path = DATA_PATH + await getPathForMediaFile(request.params.name) + } else + respond(response, 404, {statusCode: 404, message: "No path or filename provided"}) + return response.download(path) +} + +export async function copyFromLocalArchive(request: Request, response: Response) { + const path = findFile(request.body.name, ARCHIV_PATH) + if (path && path.length>0) { + var newpath = request.body.serverPath + request.body.slug; + if (copyFile(path, newpath, request.body.name)) + return respond(response, 201, { + statusCode: 201, + message: 'File fetched', + path: newpath + '/' + request.body.name, + }) + } + return respond(response, 500, {statusCode: 500, message: "File not found"}) + +} + +export async function uploadFile(request: Request, response: Response) { + +try { + var error = "create dir" + createDir(DATA_PATH+UPLOAD_TEMP_PATH) + var error = "parse form data" + const { filename, path, uploaded } = await paseFormdata(request,response) + var error = "move file" + if (moveFile(uploaded, path, filename)) + return respond(response, 201, {statusCode: 201, message: 'Created '+path+filename}); + } catch (err) { + return respond(response, 500, {statusCode: 500, message: error + ' ' + err.message}); + } + return respond(response, 500, {statusCode: 500, message: 'No File found'}); +} + +export async function paseFormdata(request: Request, response: Response) : Promise<{ uploaded: string, path: string, filename: string }> { + return new Promise((resolve) => { + var filedata = {} as any + const fields = {} as any + const busb = busboy({ headers: request.headers }) + busb.on('file', (name: string, file: any, info: any) => { + var { filename, encoding, mimeType } = info + if (filename.length==0) + filename = "tmp" + const path = DATA_PATH+UPLOAD_TEMP_PATH+filename; + var ws = fs.createWriteStream(path) + filedata = { + fieldname: name, + path, + filename, + encoding, + mimeType + } + file.on('close',()=>{ + ws.close() + }) + file.on('data',(chunk)=>{ + ws.write(chunk) + }) + }) + busb.on('field', (name : string, value: object, info: any) => { + fields[name] = value + }) + busb.on('close', () => { + resolve({ uploaded: filedata.path, path: fields.path, filename: fields.filename }) + }) + request.pipe(busb) + return + }) + +} diff --git a/backend/src/controller/Migrate.ts b/backend/src/controller/Migrate.ts new file mode 100644 index 0000000..eb60051 --- /dev/null +++ b/backend/src/controller/Migrate.ts @@ -0,0 +1,13 @@ +import { Request, Response } from "express"; +import { migrateWpEpisode } from "../services/wpMigrationService"; +import { respond } from "../tools/Controller"; + +export async function migrateEpisode(request: Request, response: Response) { + try { + const data = request.body; + await migrateWpEpisode(data.episode, (data.podcastId?data.podcastId:0)) + respond(response, 201, { statusCode: 201, message: "Episode id=" + (data as any).id + " saved successfully" }); + } catch (err) { + respond(response, 500, { statusCode: 500, message: err.message }); + } + } \ No newline at end of file diff --git a/backend/src/controller/Podcast.ts b/backend/src/controller/Podcast.ts new file mode 100644 index 0000000..5fd69be --- /dev/null +++ b/backend/src/controller/Podcast.ts @@ -0,0 +1,40 @@ +import * as fs from 'fs'; +import { Request, Response } from "express"; +import { respond } from "../tools/Controller"; +import { getAllGen, getExtQueryGen } from "../services/genericService"; +import Podcast from "../entities/Podcast"; +import Enumerator from "../entities/Enumerator"; +import { getEnumFunctions } from "../tools/Enumerations"; +import { generateRss } from "../tools/RssGenerator"; +import { FEED_SLUG } from "../tools/Configuration"; +import { createDir, dataPath } from "../tools/DataFiles"; +import { setLastUpdate } from '../services/podcastService'; + +export async function generateRssAction(request: Request, response: Response) { + try { + const slug = request.query.slug as string; + const baseUrl = process.env.podcastRssUrl ?? request.query.mediaBase as string + const podcast = await getExtQueryGen(Podcast, { + where: { + slug + }, + relations: ["episodes"] + }) as Podcast + + + const enums = await getAllGen(Enumerator) as Array + + const xml = generateRss(podcast, baseUrl, FEED_SLUG, getEnumFunctions(enums) ) + + var dir = dataPath(FEED_SLUG); + createDir(dir) + const target_file = dir + podcast.slug+".xml" + fs.writeFileSync(target_file, xml) + setLastUpdate(podcast.id) + respond(response, 201, { message: "RSS for podcast slug=" + podcast.slug + " successfully generated" }); + } catch (error) { + respond(response, 500, { + statusCode: 500, + statusMessage: "Internal server error"}) + } +} diff --git a/backend/src/controller/Serie.ts b/backend/src/controller/Serie.ts new file mode 100644 index 0000000..7c59718 --- /dev/null +++ b/backend/src/controller/Serie.ts @@ -0,0 +1,22 @@ +import Serie from "../entities/Serie"; +import { setLastAndFirst } from "../services/serieService"; +import { simpleSave } from "./All"; +import {Request, Response} from "express"; +import { respond } from "../tools/Controller"; + +export async function updateSerieAfterEpisodeChange(request: Request, response: Response) { + try { + const body = request.body as any; + if (body.hasOwnProperty('id')) { + if (body.hasOwnProperty('title')) { + simpleSave(Serie, request, response) + } else { + var slug = await setLastAndFirst(body.id) + respond(response,200,slug) + } + } else + return respond(response, 501, "nd data"); + } catch (err) { + return respond(response, 500, err.message); + } +} \ No newline at end of file diff --git a/backend/src/controller/Settings.ts b/backend/src/controller/Settings.ts new file mode 100644 index 0000000..d854aba --- /dev/null +++ b/backend/src/controller/Settings.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs'; +import { Request, Response } from "express"; +import { DATABASE_PATH, DATAFILES_PATH, EXT_MENU_AP, EXT_MENU_FILTER, EXT_MENU_BASEURL } from "../tools/Configuration"; +import { sendResponse } from '../tools/Controller'; +import { Any, EntityMetadata } from 'typeorm'; + +var menu = [] + +function linkGenerator( section: any ) : any { + const linker = item => { + if (!item.link || item.link.length <1) + if (item.page && item.page.slug && item.page.slug.length > 0) + item.link = EXT_MENU_BASEURL + item.page.slug + } + linker(section) + section.menu_items.forEach(element => linker(element)); + return section +} + +async function getExternalMenu( locale: string ) : Promise { + if (!EXT_MENU_AP.startsWith('http')) return [] + const url = EXT_MENU_AP.replace("locale",locale) + const result = await fetch(url) + const ext_menu = await result.json() as Array + return ext_menu.filter(( item: any ) => !item.link || item.link.toLowerCase().indexOf(EXT_MENU_FILTER?.toLowerCase()) === -1).map(linkGenerator) +} +async function getLocalMenu( locale: string ) : Promise { + const meta = JSON.parse(fs.readFileSync(DATABASE_PATH + DATAFILES_PATH + 'meta-'+locale+'.json',{encoding:'utf8', flag:'r'})); + console.log(EntityMetadata) + return meta.menu +} + +export async function getMetadata(request: Request, response: Response) { + const locale = request.query.locale + if (menu.length<1) { + const ext_menu = await getExternalMenu(locale as string) + const loc_menu = await getLocalMenu(locale as string) + menu = ext_menu.concat(loc_menu) + } + sendResponse( response, { menu }) +} \ No newline at end of file diff --git a/server/db/entities/Enumerator.ts b/backend/src/entities/Enumerator.ts similarity index 86% rename from server/db/entities/Enumerator.ts rename to backend/src/entities/Enumerator.ts index 9f1c9e0..3da5f05 100644 --- a/server/db/entities/Enumerator.ts +++ b/backend/src/entities/Enumerator.ts @@ -1,8 +1,7 @@ import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; -import IEnumerator from "../../../base/types/IEnumerator"; @Entity() -export default class Enumerator extends BaseEntity implements IEnumerator{ +export default class Enumerator extends BaseEntity{ @PrimaryGeneratedColumn() id: number; diff --git a/backend/src/entities/Episode.ts b/backend/src/entities/Episode.ts new file mode 100644 index 0000000..62c1ff7 --- /dev/null +++ b/backend/src/entities/Episode.ts @@ -0,0 +1,140 @@ +import { + BaseEntity, + Column, + Entity, + ManyToOne, + PrimaryGeneratedColumn, + CreateDateColumn, + DeleteDateColumn, + UpdateDateColumn, +} from "typeorm"; +import Podcast from "./Podcast"; +import Serie from "./Serie"; + + +@Entity() +export default class Episode extends BaseEntity { + + @PrimaryGeneratedColumn() + id: number; + + @Column("text") + title: string; + + @Column("text") + link: string; + + @Column("text") + slug: string; + + @Column("datetime") + pubdate: Date; + + @Column("text") + creator: string; + + @Column("text") + + @Column({ + type: 'text', + nullable: true, + }) + description: string; + + @Column({ + type: 'text', + nullable: true, + }) + subtitle: string; + + @Column("text") + keyword: string; + + @Column({ + type: 'text', + nullable: true, + }) + summary: string; + + @Column("text") + image: string; + + @Column({ + type: 'text', + nullable: true, + }) + postimage: string; + + @Column("boolean") + block: boolean; + + @Column("boolean") + explicit: boolean; + + @Column("int") + duration: number; + + @Column("int") + rawsize: number; + + @Column("int") + state: number; + + @Column({ + type: 'boolean', + default: false, + }) + draft: boolean; + + @Column({ + type: 'text', + nullable: true, + }) + video_link: string; + + @Column({ + type: 'text', + nullable: true, + }) + cross_ref: string; + + @Column({ + type: 'int', + nullable: true, + }) + external_id: number; + + @Column({ + type: 'int', + nullable: true, + }) + ext_series_id: number; + + @Column({ + type: 'int', + nullable: true, + }) + ext_podcast_id: number; + + @Column({ + type: 'text', + nullable: true, + }) + lastbuild: string; + + @ManyToOne(() => Podcast, (podcast) => podcast.episodes) + podcast: Podcast; + + @ManyToOne(() => Serie, (serie) => serie.episodes) + serie: Serie; + + @CreateDateColumn({ type: "datetime" }) + public createdAt: Date; + + @UpdateDateColumn({ type: "datetime" }) + public updatedAt: Date; + + @DeleteDateColumn({ type: "datetime" }) + public deletedAt: Date; +} + diff --git a/backend/src/entities/Podcast.ts b/backend/src/entities/Podcast.ts new file mode 100644 index 0000000..ecc8b29 --- /dev/null +++ b/backend/src/entities/Podcast.ts @@ -0,0 +1,118 @@ +import { + BaseEntity, + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, + CreateDateColumn, + DeleteDateColumn, + UpdateDateColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; +import Episode from './Episode'; + +@Entity() +export default class Podcast extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column('text') + cover_file: string; + + @Column('text') + title: string; + + @Column('text') + slug: string; + + @Column('text') + subtitle: string; + + @Column('text') + author: string; + + @Column('text') + summary: string; + + @Column('text') + description: string; + + @Column('int') + language_id: number; + + @Column('int') + category_id: number; + + @Column('int') + type_id: number; + + @Column('boolean') + explicit: boolean; + + @Column('text') + link: string; + + @Column('text') + copyright: string; + + @Column('text') + owner_name: string; + + @Column('text') + owner_email: string; + + @Column('text') + lastbuild: string; + + @Column('int') + state: number; + + @Column('int') + external_id: number; + + @Column({ + type: 'boolean', + default: false, + }) + draft: boolean; + + @Column({ + type: 'text', + default: '', + }) + apple_url: string; + + @Column({ + type: 'text', + default: '', + }) + spotify_url: string; + + @Column({ + type: 'text', + default: '', + }) + google_url: string; + + @Column({ + type: 'text', + default: '', + }) + stitcher_url: string; + + @OneToMany(() => Episode, (episode) => episode.podcast, { + cascade: true, + onDelete: 'CASCADE', + }) + episodes: Episode[]; + + @CreateDateColumn({ type: 'datetime' }) + public createdAt: Date; + + @UpdateDateColumn({ type: 'datetime' }) + public updatedAt: Date; + + @DeleteDateColumn({ type: 'datetime' }) + public deletedAt: Date; +} diff --git a/server/db/entities/Serie.ts b/backend/src/entities/Serie.ts similarity index 51% rename from server/db/entities/Serie.ts rename to backend/src/entities/Serie.ts index ce83285..cdf1f0f 100644 --- a/server/db/entities/Serie.ts +++ b/backend/src/entities/Serie.ts @@ -10,44 +10,11 @@ import { ManyToMany, JoinTable, } from "typeorm"; -import { ContentState } from "../../../base/types/ContentState"; -import ISerie from "../../../base/types/ISerie"; import Episode from "./Episode"; import Podcast from "./Podcast"; -export function initSerie(serie: Serie) { - serie.cover_file = ""; - serie.title = ""; - serie.slug = ""; - serie.subtitle = ""; - serie.description = ""; - serie.state = ContentState.draft; - serie.draft = false; - serie.lastbuild = ""; - serie.external_id = -1; - serie.firstEpisode = null; - serie.lastEpisode = null; -} - -export function getSerie(from): Serie { - var serie = new Serie(); - initSerie(serie); - serie.cover_file = from.cover_file; - serie.title = from.title; - serie.slug = from.slug; - serie.subtitle = from.subtitle; - serie.state = from.state; - serie.draft = from.draft; - serie.firstEpisode = from.firstEpisode; - serie.lastEpisode = from.lastEpisode; - if (from.hasOwnProperty("id")) serie.id = from.id; - if (from.hasOwnProperty("external_id")) serie.external_id = from.external_id; - if (from.hasOwnProperty("description")) serie.description = from.description; - return serie; -} - @Entity() -export default class Serie extends BaseEntity implements ISerie { +export default class Serie extends BaseEntity { @PrimaryGeneratedColumn() id: number; diff --git a/server/db/entities/Session.ts b/backend/src/entities/Session.ts similarity index 61% rename from server/db/entities/Session.ts rename to backend/src/entities/Session.ts index 72c1378..997df08 100644 --- a/server/db/entities/Session.ts +++ b/backend/src/entities/Session.ts @@ -1,18 +1,8 @@ import { BaseEntity, Column, CreateDateColumn, DeleteDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; -import ISession from "../../../base/types/ISession"; import User from "./User"; -export function getSession( sessiondata: ISession) { - var session = new Session(); - if (session.id>0) - session.id = sessiondata.id; - session.userId = sessiondata.userId; - session.refreshToken = sessiondata.refreshToken; - return session; -} - @Entity() -export default class Session extends BaseEntity implements ISession{ +export default class Session extends BaseEntity { @PrimaryGeneratedColumn() id: number; diff --git a/backend/src/entities/User.ts b/backend/src/entities/User.ts new file mode 100644 index 0000000..b9d8418 --- /dev/null +++ b/backend/src/entities/User.ts @@ -0,0 +1,30 @@ +import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import Session from "./Session"; + +@Entity() +export default class User extends BaseEntity { + + @PrimaryGeneratedColumn() + id: number; + + @Column("text") + username: string; + + @Column("text") + name: string; + + @Column("text") + email: string; + + @Column({ type: "text", nullable: true }) + password: string; + + @Column({ type: "text", nullable: true }) + token: string; + + @OneToMany(() => Session, (session) => session.user, { + cascade: true, + onDelete: "CASCADE", + }) + sessions: Session[]; +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..d05e7d0 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,68 @@ +import "reflect-metadata"; +import {createConnection} from "typeorm"; +import {Request, Response} from "express"; +import * as express from "express"; +import * as bodyParser from "body-parser"; +import {AppRoutes, AuthRoutes} from "./routes"; +//import cors from 'cors'; +import cors = require('cors'); +import { logger, requestLoggerMiddleware } from "./services/loggerService"; +import authMiddleware from "./authMiddleware"; +import { NextFunction } from "express-serve-static-core"; +import { initDataSource } from "./services/datasourceService"; +import { DATABASE_PATH, DATA_PATH } from "./tools/Configuration"; +const cookieParser = require('cookie-parser'); +const proxy = require('express-http-proxy'); + +// create connection with database +initDataSource().then(() => { + // create express app + const app = express(); + app.use("/s", express.static(DATA_PATH+'/s')); + app.use("/img", express.static(DATA_PATH+'/img')); + app.use("/test", express.static(DATA_PATH+'/test')); + app.use(bodyParser.json()); + app.use(cookieParser()); + + app.use(function(req, res, next) { + res.header('Access-Control-Allow-Origin', req.get('Origin') ?? 'http://localhost:3000'); + res.header('Access-Control-Allow-Credentials', 'true'); + res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS'); + res.header('Access-Control-Expose-Headers', 'Content-Length'); + res.header('Access-Control-Allow-Headers', 'Accept, Content-Type, Credentials, Authorization, Cookie, X-Requested-With, Range, refresh_token, Special-Request-Header'); + // if (req.method === 'OPTIONS') { + // return res.sendStatus(200); + // } else { + return next(); + // } + }); + // register all application routes + AppRoutes.forEach(route => { + logger(3, "Adding free route " + route.method + " " + route.path) + app[route.method](route.path, (request: Request, response: Response, next: NextFunction) => { + requestLoggerMiddleware(request) + route.action(request, response) + .then(() => next) + .catch(err => next(err)); + }); + }) + AuthRoutes.forEach(route => { + logger(3, "Adding restricted route " + route.method + " " + route.path) + app[route.method](route.path, (request: Request, response: Response, next: NextFunction) => { + requestLoggerMiddleware(request) + authMiddleware(request,response, () => { + route.action(request, response) + .then(() => next) + .catch(err => next(err)) + }); + }) + }) + + //app.use('/',proxy("http://localhost:3000")) + + // run app + app.listen(3003); + + console.log("Express application is up and running on port 3003"); + +}).catch(error => console.log("TypeORM connection error: ", error)); diff --git a/backend/src/jwt.ts b/backend/src/jwt.ts new file mode 100644 index 0000000..53a6a32 --- /dev/null +++ b/backend/src/jwt.ts @@ -0,0 +1,66 @@ +import { Response} from "express"; +import * as jwt from "jsonwebtoken" +import User from "./entities/User" +import { getTokenSecret } from "./tools/Configuration"; + +export const generateAccessToken = (user: User) => { + + return jwt.sign({ userId: user.id }, getTokenSecret( "ACCESS" ), { + expiresIn: '10m' + }) +} + +export const generateRefreshToken = (user: User) => { + + return jwt.sign({ userId: user.id }, getTokenSecret( "REFRESH" ), { + expiresIn: '2d' + }) +} + +export const generateUrlToken = (username: string, purpose: string, expiry: string) => { + + return jwt.sign({ username, purpose }, getTokenSecret( "URL" ), { + expiresIn: expiry + }) +} + +export const decodeUrlToken = (token: string) : { purpose: string, username: string } | null => { + try { + return jwt.verify(token, getTokenSecret( "URL" )) + } catch (error) { + return null + } +} + +export const decodeRefreshToken = (token: string) => { + try { + return jwt.verify(token, getTokenSecret( "REFRESH" )) + } catch (error) { + return null + } +} + +export const decodeAccessToken = (token: string) => { + try { + return jwt.verify(token, getTokenSecret( "ACCESS" )) + } catch (error) { + return null + } +} + +export const sendRefreshToken = (response: Response, token: string) => { + response.cookie('refresh_token', token, { path: '/', httpOnly: true, sameSite: true, domain: 'localhost:3003' }) +} + +export const deleteRefreshToken = (response: Response) => { + response.clearCookie('refresh_token') +} + +export const sendAuthToken = (response: Response, token: string) => { + response.cookie('auth_token', token, { path: '/', httpOnly: true, sameSite: true, domain: 'localhost:3003' }) +} + +export const deleteAuthToken = (response: Response) => { + response.clearCookie('auth_token') +} + diff --git a/backend/src/migration/.gitkeep b/backend/src/migration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/routes.ts b/backend/src/routes.ts new file mode 100644 index 0000000..6297edc --- /dev/null +++ b/backend/src/routes.ts @@ -0,0 +1,179 @@ +import { deleteQuery, getAll, getExtMany, getExtQuery, getExtQueryAll, getQuery, simpleSave, simpleSaveMultiple } from "./controller/All"; +import { checkRefreshToken, checkToken, getUserToken, login, logout, setPassword } from "./controller/Auth"; +import { count } from "./controller/Count"; +import { setEnums } from "./controller/Enums"; +import { moveEpisodeAction, saveEpisodeAction } from "./controller/Episodes"; +import { copyFromLocalArchive, downloadFileAction, fetchFileAction, uploadFile } from "./controller/Files"; +import Enumerator from "./entities/Enumerator"; +import Episode from "./entities/Episode"; +import Podcast from "./entities/Podcast"; +import Serie from "./entities/Serie"; +import { getRouteSlug } from "./tools/Configuration"; +import { getMetadata } from "./controller/Settings"; +import { generateRssAction } from "./controller/Podcast"; +import { migrateEpisode } from "./controller/Migrate"; +import { updateSerieAfterEpisodeChange } from "./controller/Serie"; + +/** + * All application routes. + */ +export const AuthRoutes = [ + { + path: getRouteSlug("USERTOKEN_AP"), + method: "get", + action: getUserToken + }, + { + path: getRouteSlug("REFRESH_AP"), + method: "post", + action: checkRefreshToken + }, + { + path: getRouteSlug("PASSWORD_AP"), + method: "post", + action: setPassword + }, + { + path: getRouteSlug("CHECK_TOKEN_AP"), + method: "get", + action: checkToken + }, + { + path: getRouteSlug("ENUMERATIONS_AP"), + method: "post", + action: setEnums + }, + { + path: getRouteSlug("EPISODE_AP"), + method: "delete", + action: (req, res) => deleteQuery(Episode,req,res) + }, + { + path: getRouteSlug("EPISODE_AP"), + method: "post", + action: saveEpisodeAction + }, + { + path: getRouteSlug("EPISODEMOVE_AP"), + method: "post", + action: moveEpisodeAction + }, + { + path: getRouteSlug("PODCAST_AP"), + method: "post", + action: (req, res) => simpleSave(Podcast, req, res) + }, + { + path: getRouteSlug("PODCAST_AP"), + method: "delete", + action: (req, res) => deleteQuery(Podcast,req,res) + }, + { + path: getRouteSlug("SERIE_AP"), + method: "delete", + action: (req, res) => deleteQuery(Serie,req,res) + }, + { + path: getRouteSlug("SERIES_AP"), + method: "post", + action: (req, res) => simpleSaveMultiple(Serie,req,res) + }, + { + path: getRouteSlug("SERIE_AP"), + method: "post", + action: (req, res) => updateSerieAfterEpisodeChange(req,res) + }, + { + path: getRouteSlug("FETCHLOCAL_AP"), + method: "post", + action: fetchFileAction + }, + { + path: getRouteSlug("FROMARCHIVE_AP"), + method: "post", + action: copyFromLocalArchive + }, + { + path: getRouteSlug("UPLOAD_AP"), + method: "post", + action: uploadFile + }, + { + path: getRouteSlug("GENERATE_RSS_AP"), + method: "get", + action: generateRssAction + }, + { + path: getRouteSlug("EPISODEWP_AP"), + method: "post", + action: migrateEpisode + }, +] + +export const AppRoutes = [ + { + path: getRouteSlug("LOGIN_AP"), + method: "post", + action: login + }, + { + path: getRouteSlug("LOGOUT_AP"), + method: "post", + action: logout + }, + { + path: getRouteSlug("COUNT_AP"), + method: "get", + action: count + }, + { + path: getRouteSlug("ENUMERATIONS_AP"), + method: "get", + action: (req, res) => getAll(Enumerator, req, res) + }, + { + path: getRouteSlug("EPISODE_AP"), + method: "get", + action: (req, res) => getQuery(Episode, ["podcast", "serie"], req, res) + }, + { + path: getRouteSlug("EPISODES_AP"), + method: "get", + action: (req, res) => getExtMany(Episode, ["podcast", "serie"], req, res) + }, + { + path: getRouteSlug("PODCASTS_AP"), + method: "get", + action: (req, res) => getAll(Podcast, req, res) + }, + { + path: getRouteSlug("PODCAST_AP"), + method: "get", + action: (req, res) => getQuery(Podcast, ["episodes"], req, res) + }, + { + path: getRouteSlug("SERIES_AP"), + method: "get", + action: (req, res) => getExtQueryAll(Serie, { + where: {}, + order: { + lastEpisode: 'DESC' + }, + }, req, res) + }, + { + path: getRouteSlug("SERIE_AP"), + method: "get", + action: (req, res) => getQuery(Serie, ["episodes" ], req, res) + }, + { + path: getRouteSlug("FILES_AP"), + method: "get", + action: downloadFileAction + }, + { + path: getRouteSlug("METADATA_AP"), + method: "get", + action: getMetadata + } +]; \ No newline at end of file diff --git a/server/db/initdata.ts b/backend/src/services/datasourceService.ts similarity index 75% rename from server/db/initdata.ts rename to backend/src/services/datasourceService.ts index 039665a..32cd54d 100644 --- a/server/db/initdata.ts +++ b/backend/src/services/datasourceService.ts @@ -1,24 +1,78 @@ import { DataSource } from "typeorm"; -import Enumerator, { getEnumerator } from "./entities/Enumerator"; -import User from "./entities/User"; -import bcrypt from "bcrypt" -import getSecSettings from "../security"; - -export async function addAdmin(db: DataSource) { - const config = getSecSettings(); - const user = await db.manager.find(User, { where: { username: config.adminUser }}) +import Enumerator, { getEnumerator } from "../entities/Enumerator"; +import * as bcrypt from "bcrypt" +import User from "../entities/User"; +import { logger } from "./loggerService"; +import { DATABASE_FILE, DATABASE_PATH } from "../tools/Configuration"; + +export default function getRepository(T) { return dataSource.getRepository(T) } +export function getDbManager() { return dataSource.manager } + +const appDataSource = new DataSource({ + "type": "sqlite", + "database": DATABASE_PATH + "/" + DATABASE_FILE, + "synchronize": true, + "logging": false, + "entities": [ + "src/entities/**/*.ts" + ], + "migrations": [ + "src/migrations/**/*.ts" + ], + "subscribers": [ + "src/subscribers/**/*.ts" + ], + // "cli": { + // "entitiesDir": "src/entity", + // "migrationsDir": "src/migration", + // "subscribersDir": "src/subscriber" + // } +}) + +export function setTestDataSource(TstDataSource: DataSource) { + dataSource = TstDataSource +} + +var dataSource: DataSource = null + +export async function initDataSource(): Promise { + if (!dataSource) + dataSource = appDataSource; + if (dataSource.isInitialized) return dataSource; + else { + console.log("init db") + await dataSource.initialize(); + console.log("db initialized") + await dataSource.runMigrations(); + console.log("db migrated") + const german = await dataSource.manager.findOneBy(Enumerator, { + shorttext: "de-DE", + }); + if (!german) { + console.log("init data") + await fillDefaultEnums(dataSource); + } + await addAdmin(dataSource); + return dataSource; + } + } + +async function addAdmin(db: DataSource) { + const user = await db.manager.find(User, { where: { username: process.env.ADMIN_USER || "Admin" }}) if (user.length>0) return; const adminuser = new User(); - adminuser.username=config.ADMIN_USER; + adminuser.username= process.env.ADMIN_USER ?? "Admin"; adminuser.name='Administrator' adminuser.email='' const salt = bcrypt.genSaltSync(5); - adminuser.password = bcrypt.hashSync(config.ADMIN_PASSWORD, salt); - return await db.manager.save(adminuser); + adminuser.password = bcrypt.hashSync(process.env.ADMIN_PASSWORD ?? "AdminPassword", salt); + const ret = await db.manager.save(adminuser); + logger(3,"Created Admin " + adminuser.username) + return ret } -export async function fillDefaultEnums(db: DataSource) { +async function fillDefaultEnums(db: DataSource) { const list = [ ["deutsch", "de-DE", "language", 0, 1], ["english", "en-US", "language", 0, 2], @@ -139,6 +193,7 @@ export async function fillDefaultEnums(db: DataSource) { return await db.manager.transaction(async (manager) => { return await manager.save( list.map((item) => { + logger(3, "Init db Enum "+JSON.stringify(item).substring(0,50)) return getEnumerator({ displaytext: item[0] as string, shorttext: item[1] as string, diff --git a/server/db/entities/Episode.ts b/backend/src/services/episodeService.ts similarity index 50% rename from server/db/entities/Episode.ts rename to backend/src/services/episodeService.ts index fece685..7835bad 100644 --- a/server/db/entities/Episode.ts +++ b/backend/src/services/episodeService.ts @@ -1,19 +1,43 @@ -import { - BaseEntity, - Column, - Entity, - ManyToOne, - PrimaryGeneratedColumn, - CreateDateColumn, - DeleteDateColumn, - UpdateDateColumn, -} from "typeorm"; -import IEpisode from "../../../base/types/IEpisode"; -import Podcast from "./Podcast"; -import Serie from "./Serie"; - +import Episode from "../entities/Episode"; +import { isUpdate, saveGen, updateGen } from "./genericService"; +import { setLastAndFirst } from "./serieService"; +import writeTags from "../tools/Id3Tagger"; +import Serie from "../entities/Serie"; +import Podcast from "../entities/Podcast"; +import getRepository, { getDbManager } from "./datasourceService"; +import { isQualifiedUrl } from "../tools/Urls"; +import { dataPath, getFilename, moveFile } from "../tools/DataFiles"; +import { SERVER_MP3_PATH } from "../tools/Configuration"; +import { Like } from "typeorm"; + +export async function saveEpisode(episode : Episode): Promise { + if( isUpdate(episode) ) { + const record = await getRepository(Episode).preload(episode) as Episode + await updateGen(Episode, record) + if (episode.serie) + setLastAndFirst(episode.serie.id) + return episode; + } else { + const id = await saveGen(Episode, episode) + const epi = getEpisode({ ...episode, id }) as Episode + writeTags(epi) + return epi; + } +}; + +export async function moveEpisode(episode : Episode, podcast: Podcast, serie: Serie): Promise { + if (!isQualifiedUrl(episode.link)) { + const file = getFilename(episode.link) + const newpath = SERVER_MP3_PATH + podcast.slug + await moveFile(dataPath(episode.link), newpath, file) + episode.link = newpath + '/' + file + } + episode.podcast = podcast + episode.serie = serie + return await saveEpisode(episode) +} -export function getEpisode(from) { +export function getEpisode(from: any) : Episode { var episode = new Episode(); episode.id = from.id; episode.image = from.image; @@ -68,129 +92,13 @@ export function joinEpisodePodcastAndSerie(episode: Episode, podcast: Podcast, s } } -@Entity() -export default class Episode extends BaseEntity implements IEpisode { - - @PrimaryGeneratedColumn() - id: number; - - @Column("text") - title: string; - - @Column("text") - link: string; - - @Column("text") - slug: string; - - @Column("datetime") - pubdate: Date; - - @Column("text") - creator: string; - - @Column("text") - - @Column({ - type: 'text', - nullable: true, - }) - description: string; - - @Column({ - type: 'text', - nullable: true, - }) - subtitle: string; - - @Column("text") - keyword: string; - - @Column({ - type: 'text', - nullable: true, - }) - summary: string; - - @Column("text") - image: string; - - @Column({ - type: 'text', - nullable: true, - }) - postimage: string; - - @Column("boolean") - block: boolean; - - @Column("boolean") - explicit: boolean; - - @Column("int") - duration: number; - - @Column("int") - rawsize: number; - - @Column("int") - state: number; - - @Column({ - type: 'boolean', - default: false, - }) - draft: boolean; - - @Column({ - type: 'text', - nullable: true, - }) - video_link: string; - - @Column({ - type: 'text', - nullable: true, - }) - cross_ref: string; - - @Column({ - type: 'int', - nullable: true, - }) - external_id: number; - - @Column({ - type: 'int', - nullable: true, - }) - ext_series_id: number; - - @Column({ - type: 'int', - nullable: true, - }) - ext_podcast_id: number; - - @Column({ - type: 'text', - nullable: true, - }) - lastbuild: string; - - @ManyToOne(() => Podcast, (podcast) => podcast.episodes) - podcast: Podcast; - - @ManyToOne(() => Serie, (serie) => serie.episodes) - serie: Serie; - - @CreateDateColumn({ type: "datetime" }) - public createdAt: Date; - - @UpdateDateColumn({ type: "datetime" }) - public updatedAt: Date; - - @DeleteDateColumn({ type: "datetime" }) - public deletedAt: Date; -} +export const getPathForMediaFile = async function (filename: string): Promise { + var tmpQuery = { + where: { + link: Like("%"+filename) + } + }; + var result = await getRepository(Episode).findOneOrFail(tmpQuery); + return result.link +} \ No newline at end of file diff --git a/backend/src/services/filesService.ts b/backend/src/services/filesService.ts new file mode 100644 index 0000000..fb60410 --- /dev/null +++ b/backend/src/services/filesService.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs'; +import fetch from 'node-fetch' +import { createDir, dataPath } from '../tools/DataFiles'; +export interface IFetchFileResult { + statusCode: number, + message: string, + path?: string +} + +export async function getFileFromUrl( orgUrl: string, toPath: string, altPath: string, toFile: string) : Promise { + return new Promise(async (resolve, reject) => { + const path = dataPath(toPath); + const altpath = dataPath(altPath); + const filename = path + '/' + toFile; + const alternative = altpath + '/' + toFile; + if (fs.existsSync(filename)) + resolve({ + statusCode: 423, + message: 'File already exists', + path: filename, + }); + else if ( + altPath && + altPath.length > 0 && + fs.existsSync(alternative) + ) { + resolve({ + statusCode: 423, + message: 'File already exists', + path: alternative, + }); + } else { + try { + createDir(path); + const data = await fetch(orgUrl) + const buffer = Buffer.from(await data.arrayBuffer()); + fs.writeFileSync(filename, buffer); + } catch (err) { + resolve({ statusCode: 400, message: err.message }); + return; + } + resolve({ + statusCode: 201, + message: 'File fetched', + path: filename, + }); + } + }); +} \ No newline at end of file diff --git a/backend/src/services/genericService.ts b/backend/src/services/genericService.ts new file mode 100644 index 0000000..aac7401 --- /dev/null +++ b/backend/src/services/genericService.ts @@ -0,0 +1,89 @@ +import { ObjectLiteral, UpdateResult } from "typeorm"; +import getRepository, { getDbManager } from "./datasourceService"; +import { logger } from "./loggerService"; + + + +export async function getAllGen(T: any) : Promise { + + const repository = getRepository(T); + return await repository.find(); +} + +export async function getExtManyGen(T: any, extQuery: any) : Promise { + + // get a post repository to perform operations with post + const repository = getRepository(T); + + // load posts + return await repository.find(extQuery); +} + +export async function getExtQueryGen(T: any, extQuery: any) : Promise { + + // get a post repository to perform operations with post + const repository = getRepository(T); + + // load posts + return await repository.findOne(extQuery); +} + +export async function getExtQueryAllGen(T: any, extQuery: any) : Promise> { + + // get a post repository to perform operations with post + const repository = getRepository(T); + + // load posts + return await repository.find(extQuery); +} + + +export async function getQueryGen(T: any, query: any) : Promise { + + // get a post repository to perform operations with post + const repository = getRepository(T); + + // load posts + return await repository.findOne({ where: query}); +} + +export async function getByIdGen(T: any, id: number) : Promise { + + // get a post repository to perform operations with post + const repository = getRepository(T); + + // load posts + return await repository.findOne({ where: { id }}); +} + +export async function deleteIdGen(T: any, id: number) : Promise { + + const manager = getDbManager(); + const result = await manager.softDelete(T, id); + return (result && result.affected == 1) +} + +export async function deleteGen(T: any, query: ObjectLiteral) : Promise { + const manager = getDbManager(); + const result = await manager.softDelete(T,query); + return (result && result.affected >= 1) +} + +export function isUpdate(podcastObject: any): Boolean { + if (!podcastObject) return false; + var isupdate: Boolean = 'id' in podcastObject; + if (isupdate) isupdate = podcastObject.id as number > 0; + return isupdate; +}; + +export async function saveGen( T: any, object: T ): Promise { + var entity = getDbManager().create(T, object ) as T + const res = await getDbManager().insert(T,entity); + return (res.identifiers && res.identifiers[0]?res.identifiers[0].id:undefined) +}; + + export async function updateGen(T: any, object: T) : Promise { + const res = await getDbManager().update(T, (object as any).id, object); + return (res.affected && res.affected==1?(object as any).id:undefined) +}; + diff --git a/backend/src/services/loggerService.ts b/backend/src/services/loggerService.ts new file mode 100644 index 0000000..61f27ae --- /dev/null +++ b/backend/src/services/loggerService.ts @@ -0,0 +1,47 @@ +import {Request, Response} from "express"; +import * as fs from 'fs' +import { getRandomFileName } from "../tools/DataFiles"; + +const recorder = false + +var loglevel = 3 + +export const deleteOldTempFiles = (directory: string, ageInMinutes: number) => { + fs.readdirSync(directory).forEach(file => { + const isOlder = fs.statSync(directory+file).ctime.getTime() < (Date.now() - ageInMinutes * 60000) + if (isOlder) { + fs.unlinkSync(directory+file) + } + + }) +} + +export const logger = (level:number, message: string) => { + if (level<=loglevel) + console.log(new Date(), level+" "+message) +} + +export function logResponse(response: Response, code: number, body: any) { + if (code>=500) + logger(2, "response code: " +code+ " body " + JSON.stringify(body)) + else + logger(2, "response code: " +code+ " body " + JSON.stringify(body).substring(0, 50) + " ... ") + if (code>=200 && code<400 && recorder) { + fs.writeFileSync('./data/responses/'+response.req.method+'-'+response.req.url.split('/')[2]+'-'+getRandomFileName()+".json", JSON.stringify(body)) + deleteOldTempFiles('./data/responses/',5) + } +} + +export function requestLoggerMiddleware(request: Request) { + logger(2, request.method +" "+ request.url + " " + request.headers.authorization?.substring(0,15)+ " " + JSON.stringify(request.cookies).substring(0,40)) + if (recorder) { + const data = { + method: request.method, + url: request.url, + headers: request.headers, + body: (request.url.startsWith('/api/upload')?'uploadData':request.body) + } + fs.writeFileSync('./data/requests/'+request.method+'-'+request.url.split('/')[2]+'-'+getRandomFileName()+".json", JSON.stringify(data)) + deleteOldTempFiles('./data/requests/',5) + } +} \ No newline at end of file diff --git a/backend/src/services/podcastService.ts b/backend/src/services/podcastService.ts new file mode 100644 index 0000000..4bf9301 --- /dev/null +++ b/backend/src/services/podcastService.ts @@ -0,0 +1,54 @@ +import Enumerator from "../entities/Enumerator"; +import Podcast from "../entities/Podcast"; +import { createDir, dataPath, writeRss } from "../tools/DataFiles"; +import Enumerations from "../tools/Enumerations"; +import { generateRss } from "../tools/RssGenerator"; +import getRepository from "./datasourceService"; +import { getQueryGen, updateGen } from "./genericService"; + +export function getPodcast(from : any): Podcast { + var podcast = new Podcast(); + if (!from) return podcast; + if ('id' in from) podcast.id = from.id; + podcast.cover_file = from.cover_file; + podcast.title = from.title; + podcast.slug = from.slug; + podcast.subtitle = from.subtitle; + podcast.author = from.author; + podcast.summary = from.summary; + podcast.description = from.description; + podcast.language_id = from.language_id; + podcast.category_id = from.category_id; + podcast.type_id = from.type_id; + podcast.explicit = from.explicit; + podcast.link = from.link; + podcast.draft = from.draft; + podcast.copyright = from.copyright; + podcast.owner_name = from.owner_name; + podcast.owner_email = from.owner_email; + if (from.hasOwnProperty('state')) podcast.state = from.state; + if (from.hasOwnProperty('lastbuild')) podcast.lastbuild = from.lastbuild; + if (from.hasOwnProperty('external_id')) + podcast.external_id = from.external_id; + if (from.hasOwnProperty('apple_url')) + podcast.apple_url = from.apple_url; + if (from.hasOwnProperty('spotify_url')) + podcast.spotify_url = from.spotify_url; + if (from.hasOwnProperty('google_url')) + podcast.google_url = from.google_url; + if (from.hasOwnProperty('stitcher_url')) + podcast.stitcher_url = from.stitcher_url; + return podcast; + } + + export const setLastUpdate = async ( id: number ) : Promise => { + const podcast = await getQueryGen(Podcast, { id: id }) + podcast.lastbuild = Date.now().toLocaleString() + podcast.updatedAt = Date.now() + updateGen(Podcast, podcast as Podcast) + return podcast.lastbuild + } + + +// if (object.hasOwnProperty("updatedAt")) +// (object as any).updatedAt = new Date(); \ No newline at end of file diff --git a/backend/src/services/serieService.ts b/backend/src/services/serieService.ts new file mode 100644 index 0000000..5bdbf4e --- /dev/null +++ b/backend/src/services/serieService.ts @@ -0,0 +1,35 @@ +import Serie from "../entities/Serie" +import { getQueryGen, updateGen } from "./genericService" + +export const setLastAndFirst = async ( id: number ) : Promise => { + const serie = await getQueryGen(Serie, { id: id }) + var minmax = serie.episodes?.reduce( (minmax, episode) => { + const d = new Date(episode.pubdate) + if (d< minmax.min) + minmax.min = d + if (d > minmax.max) + minmax.max = d + return minmax + }, { min: new Date(), max: new Date(1900)}) + serie.lastEpisode = minmax?.max + serie.firstEpisode = minmax?.min + updateGen(Serie, serie as Serie) + return serie.slug + } + + +export function getSerie(from: any): Serie { + var serie = new Serie(); + serie.cover_file = from.cover_file; + serie.title = from.title; + serie.slug = from.slug; + serie.subtitle = from.subtitle; + serie.state = from.state; + serie.draft = from.draft; + serie.firstEpisode = from.firstEpisode; + serie.lastEpisode = from.lastEpisode; + if (from.hasOwnProperty("id")) serie.id = from.id; + if (from.hasOwnProperty("external_id")) serie.external_id = from.external_id; + if (from.hasOwnProperty("description")) serie.description = from.description; + return serie; + } \ No newline at end of file diff --git a/backend/src/services/sessionService.ts b/backend/src/services/sessionService.ts new file mode 100644 index 0000000..0c577d9 --- /dev/null +++ b/backend/src/services/sessionService.ts @@ -0,0 +1,75 @@ +import {Request, Response} from "express"; +import Session from '../entities/Session'; +import User from '../entities/User'; +import { generateAccessToken, sendAuthToken } from '../jwt'; +import {MoreThan} from 'typeorm'; +import { getUserById, sanitizeUserForFrontend } from './userService'; +import { deleteGen, getExtQueryGen, saveGen } from './genericService'; + +export async function createSession( refreshToken: string, userId: number ) : Promise { + const user = await getUserById(userId) + const session = new Session() + session.userId = userId + session.refreshToken = refreshToken + session.user = user + const id = await saveGen(Session, session); + return { ...session, id } as Session +} + +export async function readSession(query): Promise { + var tmpQuery = { + where: query, + relations: ["user"], + }; + return await getExtQueryGen(Session,tmpQuery) as Session +} + +export const removeSession = async (token: string) => { + var tmpQuery = { refreshToken: token } + return await deleteGen(Session,tmpQuery) +} +export const removeSessions = async (userId: number) => { + var tmpQuery = { userId } + return await deleteGen(Session, tmpQuery) +} +export const removeOldSessions = async (date: Date) => { + var tmpQuery = { updatedAt: MoreThan(date) } + return await deleteGen(Session, tmpQuery) +} + +export async function getSessionByToken(token: string): Promise { + return await readSession({refreshToken: token}) +} + + +export async function makeSession(user: User, response: Response): Promise { + const authToken = generateAccessToken(user) + const refreshToken = generateAccessToken(user) + const session = await createSession(authToken, user.id) + const userId = session.userId + + if (userId) { + sendAuthToken( response, authToken ) + return getUserBySessionToken(authToken) + } + + throw Error('Error Creating Session') +} + +export async function getUserBySessionToken(authToken: string): Promise { + const session = await getSessionByToken(authToken) + if (!session) + return null + return sanitizeUserForFrontend(session.user) +} + +export async function checkAuthentication(authToken: string, maxAgeInMin) : Promise { + const session = await readSession({authToken: authToken}) + if (!session) + return false; + if (maxAgeInMin<1) + return true; + const creation = new Date(session.createdAt); + const age = Date.now().valueOf() - creation.valueOf(); + return (age/60000) < maxAgeInMin; +} diff --git a/backend/src/services/userService.ts b/backend/src/services/userService.ts new file mode 100644 index 0000000..65b26b4 --- /dev/null +++ b/backend/src/services/userService.ts @@ -0,0 +1,65 @@ +import { INVITE_TIME, INVITE_TOKEN, PWRESET_TIME } from '../tools/Configuration'; +import * as jwt from '../jwt' +import { getExtQueryGen, getQueryGen, isUpdate, saveGen, updateGen } from './genericService'; +import User from '../entities/User'; + +export async function getUserByEmail(email: string): Promise { + return getQueryGen(User , { email: email }) as Promise; +} + +export async function getUserByUserName(username: string): Promise { + return getQueryGen(User , { username: username } ) as Promise; +} + +export async function getUserById( id: number ): Promise { + return getQueryGen(User , { id: id } )as Promise; +} + +export async function readUser(query): Promise { + var tmpQuery = { + where: query, + relations: ["sessions"], + }; + var result = await getExtQueryGen(User, tmpQuery) as User; + return result +} + +export async function createUser(user: User) : Promise { + const id = await saveGen(User, user); + return { ...user, id } as User +} + +export async function updateUser(user: User) : Promise { + const id = await updateGen(User, user); + return { ...user, id } as User +} + + +export async function createUserWithToken(data: User, type: string) : Promise { + var user = await getQueryGen(User, { username: data.username }) as User + var id:number + if (!user) { + user = { ...data } as User; + user.name = data.username + user.email = "" + } + user.token = jwt.generateUrlToken( data.username, type, (type==INVITE_TOKEN?INVITE_TIME:PWRESET_TIME) ) + if (isUpdate(user)) + id = await updateGen(User,user) + else + id = await saveGen(User,user) + return { ...user, id } as User +} + +export function sanitizeUserForFrontend(user: User | undefined): User { + + if (!user) { + return user + } + + delete user.password + delete user.sessions + + return user +} + diff --git a/backend/src/services/wpMigrationService.ts b/backend/src/services/wpMigrationService.ts new file mode 100644 index 0000000..a0a6174 --- /dev/null +++ b/backend/src/services/wpMigrationService.ts @@ -0,0 +1,31 @@ +import Episode from '../entities/Episode'; +import Serie from '../entities/Serie'; +import Podcast from '../entities/Podcast'; +import { joinEpisodePodcastAndSerie } from './episodeService'; +import { getByIdGen, getExtQueryGen, saveGen } from './genericService'; + +export async function migrateWpEpisode(episode: Episode, podcastId: number) : Promise { + + const getRelation = async (T:any, id: number, externalId: number) => { + if (id>0) + return await getByIdGen(T, podcastId ); + else + return await getExtQueryGen(T,{ + where: { external_id: externalId }, + relations: ['episodes'] + })[0] + } + + const podcast = await getRelation(Podcast,podcastId,episode.ext_podcast_id) + const serie = await getRelation(Serie,-1,episode.ext_series_id) + + joinEpisodePodcastAndSerie(episode,podcast,serie) + + const id = await saveGen(Episode,episode); + if (serie) + await saveGen(Podcast,serie); + if (podcast) + await saveGen(Podcast,podcast); + return { ...episode, id } as Episode +} + diff --git a/backend/src/subscriber/.gitkeep b/backend/src/subscriber/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/tests/Configuration.test.ts b/backend/src/tests/Configuration.test.ts new file mode 100644 index 0000000..ed39ffe --- /dev/null +++ b/backend/src/tests/Configuration.test.ts @@ -0,0 +1,28 @@ +import { ROUTES, getRouteSlug } from "../tools/Configuration"; + +describe("Configuration", () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules() // Most important - it clears the cache + process.env = { ...OLD_ENV }; // Make a copy + }); + afterAll(() => { + process.env = OLD_ENV; // Restore old environment + }); + test("getRouteSlug returns the configured value plus prefix", () => { + delete process.env.ROUTE_PREFIX + const actualResult = getRouteSlug("USERTOKEN_AP"); + expect(actualResult).toBe("/api/" + ROUTES.USERTOKEN_AP); + }); + test("returns the configured value plus prefix", () => { + process.env.ROUTE_PREFIX = "/opi/" + const actualResult = getRouteSlug("USERTOKEN_AP"); + expect(actualResult).toBe("/opi/" + ROUTES.USERTOKEN_AP); + }); + test("returns the configured value plus prefix and param", () => { + process.env.ROUTE_PREFIX = "/opi/" + const actualResult = getRouteSlug("CHECK_TOKEN_AP", true); + expect(actualResult).toBe("/opi/" + ROUTES.CHECK_TOKEN_AP + ROUTES.CHECK_TOKEN_PARAM); +}); +}); diff --git a/backend/src/tools/Configuration.ts b/backend/src/tools/Configuration.ts new file mode 100644 index 0000000..49e1b79 --- /dev/null +++ b/backend/src/tools/Configuration.ts @@ -0,0 +1,81 @@ +require('dotenv').config() + +export const DATABASE_PATH = process.env.DATABASE_PATH ?? 'data'; +export const DATA_PATH = process.env.DATA_PATH ?? 'public'; +export const DATABASE_FILE = process.env.DATABASE_FILE ?? "podcasts.sqlite" +export const EXT_MENU_AP = process.env.EXT_MENU_AP ?? "" +export const EXT_MENU_FILTER = process.env.EXT_MENU_FILTER ?? "" +export const EXT_MENU_BASEURL = process.env.EXT_MENU_BASEURL ?? "" +export const DATAFILES_PATH = '/files/' +export const SERVER_IMG_PATH = '/s/covers/'; +export const SERVER_POSTIMG_PATH = '/s/posts/'; +export const SERVER_MP3_PATH = '/s/audio/'; +export const FEED_SLUG = '/s/feed/'; +export const UPLOAD_TEMP_PATH = '/s/upload/'; + +export const ARCHIV_PATH ="archiv"; + +export const ROUTE_PREFIX_DEFAULT = "/api/" +export const ROUTES = { + AUTHUSER_AP: 'auth/user', + CHECK_TOKEN_AP: 'auth/checktoken', + CHECK_TOKEN_PARAM: '?token=', + COUNT_AP: 'count', + ENUMERATIONS_AP: 'enums', + EPISODE_AP: 'episode', + EPISODES_AP: 'episodes', + EPISODEWP_AP: 'episodewp', + EPISODEMOVE_AP: 'episodemove', + FILES_AP: 'files', + FILES_PARAM: '?path=', + FETCHLOCAL_AP: 'fetchfile', + FROMARCHIVE_AP: 'fromarchive', + GENERATE_RSS_AP: 'generaterss', + GENERATE_RSS_PARAM: '?slug=', + LOGIN_AP: 'auth/login', + LOGOUT_AP: 'auth/logout', + METADATA_AP: 'meta', + METADATA_PARAM: '?locale=', + PASSWORD_AP: 'auth/password', + PODCAST_AP: 'podcast', + PODCASTS_AP: 'podcasts', + REFRESH_AP: 'auth/refresh', + SERIES_AP: 'series', + SERIE_AP: 'serie', + UPLOAD_AP: 'upload', + USERTOKEN_AP: 'auth/usertoken', + USERTOKEN_PARAM: '?username=%%&type=', +} + +export function getRouteSlug(route: string, prepareParam = false) { + const prefix = process.env.ROUTE_PREFIX ?? ROUTE_PREFIX_DEFAULT + var paramName = route.replace("AP","PARAM") + if (prepareParam && ROUTES.hasOwnProperty(paramName)) + return prefix + ROUTES[route] + ROUTES[paramName] + return prefix + ROUTES[route] +} + +export function getTokenSecret( type: string ) : string { + switch(type) { + case 'REFRESH': + return process.env.JWT_REFRESH_TOKEN_SECRET ?? "CiHbNFoNGhFZjybkKRQidWRnJdKjcHRTT8jvNTbjaBKCKBwpyCL2LBHsXq77dP" + case 'ACCESS': + return process.env.JWT_ACCESS_TOKEN_SECRET ?? "N3GzpyEGdHjb9hmEfZmfCgif3jepjk5ECHsfxzEkv53mxexMQoFSyta3oL76Fsr2" + default: + return process.env.JWT_URL_TOKEN_SECRET ?? "gdQD3NPk38HTvw5T3B2CzrPyRCdJkp8NcM4D3wucFoqGp8GGB5Dm84nauFaTTyjG" + } +} + +export const INVITE_TOKEN = 'invitation'; +export const INVITE_TIME = '2d'; +export const PWRESET_TIME = '1d'; + + +// export const SETPASS_LINK= 'admin/setpassword?token='; +// export const WP_API_SLUG = 'wp-json/wp/v2/'; +// export const WP_PER_PAGE = '?per_page=100'; +// export const WP_MIN_PER_PAGE = '?per_page=1'; +// export const WP_HEADER_TOTAL = 'x-wp-total'; +// export const ROUTE_NEWPODCAST = 'newpodcast'; +// export const NUM_PAGINATION_LINKS = 7 +// export const NUM_ITEMS_PER_PAGE = 6 \ No newline at end of file diff --git a/backend/src/tools/Controller.ts b/backend/src/tools/Controller.ts new file mode 100644 index 0000000..0b55029 --- /dev/null +++ b/backend/src/tools/Controller.ts @@ -0,0 +1,17 @@ +import {Response} from "express"; +import { logResponse, logger } from "../services/loggerService"; + +export function respond( response: Response, code: number, body: any ) { + const cod = code || 500 + var bod = (body || {}) + response.status(cod).send(bod); + logResponse(response, cod, bod) +} + +export function sendResponse( response: Response, body: any ) { + respond(response,200,body) +} + +export function sendResponseCode( response: Response, code: number ) { + respond(response,code, {}) +} \ No newline at end of file diff --git a/backend/src/tools/Converters.ts b/backend/src/tools/Converters.ts new file mode 100644 index 0000000..9278874 --- /dev/null +++ b/backend/src/tools/Converters.ts @@ -0,0 +1,74 @@ +export function strToDurationInSec(durationStr: string) { + if (!durationStr) return -1; + const parts = durationStr.replace(/[\.-\/\\]/g, ':').split(":"); + var pos = 0; + var sum = 0; + while (parts.length > 0 && parts[0].length > 0) { + sum *= 60; + sum += Number(parts[pos]); + pos++; + if (pos == parts.length) return sum; + } + return sum; +} +export function durationInSecToStr(duration: number | string) { + if (typeof(duration)=="string") + return duration; + if (duration <= 0) return "0:00"; + var result = [ + Math.floor(duration / 60 / 60), + Math.floor(duration / 60) % 60, + Math.floor(duration % 60), + ] + .join(":") + .replace(/\b(\d)\b/g, "0$1"); + if (result.startsWith("00:")) result = result.substring(1); + if (result.length>5 && result.startsWith("0:")) result = result.substring(2); + if (result.startsWith("0")) result = result.substring(1); + if (result.startsWith(":")) result = "0" + result; + return result; +} + +const datePattern = + /^(0?[1-9]|[1-2][0-9]|3[01])([-\/.])(0?[1-9]|1[012])\2(19|20)?(\d\d)$/gm; +const datePatter2 = + /^(19|20)?(\d\d)([-\/.])(0?[1-9]|1[012])\3(3[01]|[12][0-9]|0?[1-9])/g; +export function strToDate(str: string): Date { + var result = str; + if (str.match(datePattern)) result = str.replace(datePattern, "$4$5-$3-$1"); + else { + const match = str.match(datePatter2); + if (match) result = match[0]; + } + return new Date(result); +} + +export function dateToIsoString(date: Date) { + return ( + date.getFullYear() + + "-" + + ("0" + (date.getMonth() + 1)).slice(-2) + + "-" + + ("0" + (date.getDate())).slice(-2) + ); +} + +export function saveSlugFormText(text: string, lowercase = true): string { + var slug = (lowercase?text.toLowerCase():text); + return slug + .replace(/([:;\.,]+\s?)+/g, "_") + .replace(/([!'/()*"~#@?%&\\:;,<>\*\+\|]|\DE\/EN)+/g, "") + .replace(" deen", "") + .replace("ä", "ae") + .replace("ö", "oe") + .replace("ü", "ue") + .replace("ß", "ss") + .replace(/\s+/g, "_"); +} + +export function getSaveFilename(name: string): string { + const ext_index = name.lastIndexOf('.') + const base = name.slice(0,ext_index) + const ext = name.slice(ext_index) + return saveSlugFormText(base,false)+ext +} diff --git a/backend/src/tools/DataFiles.ts b/backend/src/tools/DataFiles.ts new file mode 100644 index 0000000..d73cde2 --- /dev/null +++ b/backend/src/tools/DataFiles.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as path from 'path' +import { DATA_PATH } from './Configuration'; + +export const getRandomFileName = () => { + var timestamp = new Date().toISOString().replace(/[-:.]/g,""); + var random = ("" + Math.random()).substring(2, 8); + var random_number = timestamp+random; + return random_number; +} + +export const dataPath = (path: string) : string => { + return DATA_PATH + path; +}; + +export const createDir = (dir: string) : void => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +}; + +export const moveFile = function (fullpath: string, newpath : string, filename: string) { + var dir = dataPath(newpath); + createDir(dir); + const target_path = dir + '/' + filename; + fs.renameSync(fullpath, target_path); + return true; +}; + +export const copyFile = function (fullpath: string, newpath : string, filename: string) { + var dir = dataPath(newpath); + createDir(dir); + const target_path = dir + '/' + filename; + fs.copyFileSync(fullpath, target_path); + return true; +}; + +export const writeRss = function(feedSlug: string, podcastSlug: string, xml: string) { + var dir = dataPath(feedSlug); + createDir(dir) + const target_file = dir + podcastSlug+".xml" + fs.writeFileSync(target_file, xml) +} + +export const getFilename = function(target: string) { + return target.split("/").slice(-1)[0]; +} + + +export function findFile(name: string, path: string) : string { + const dirs = [path] + while (dirs.length>0) { + const currentDir = dirs.pop() + const items = fs.readdirSync(currentDir); + for (const item of items) { + if (!(fs.lstatSync(`${currentDir}/${item}`)).isDirectory()) { + if (item===name) + return `${currentDir}/${item}` + } else + dirs.push(`${currentDir}/${item}`) + } + } + return "" +} \ No newline at end of file diff --git a/backend/src/tools/Enumerations.ts b/backend/src/tools/Enumerations.ts new file mode 100644 index 0000000..9c4235b --- /dev/null +++ b/backend/src/tools/Enumerations.ts @@ -0,0 +1,101 @@ +import Enumerator from "../entities/Enumerator"; + +export enum EnumKey { + Language = 0, + PodcastGenres = 1, + PodcastTypes = 2, + State = 3, + Authors = 4, + Tags = 5, +} + +export function getEmptyEnum(): Enumerator { + var enumer = { + displaytext: "", + shorttext: "", + parentCategory: "", + enumkey_id: -1, + enumvalue_id: -1, + }; + return enumer as Enumerator; +} +export default class Enumerations { + public static isInitialized(list: Array): boolean { + return list.length > 0; + } + + public static getEnumeration( + enumKey: EnumKey, + list: Array + ): Array { + return list.filter((item) => item.enumkey_id === enumKey); + } + + public static byIdTextList( + enums: Array, + ids: Array + ): string { + let result = enums + .filter((item) => ids.includes(item.enumvalue_id)) + .reduce((prevItem, item) => { + if (prevItem && prevItem.length > 0) + return prevItem + ", " + item.displaytext; + else return item.displaytext; + }, ""); + return result; + } + + public static byIdOne(enums: Array, id: number): Enumerator { + let result = enums.find((item) => item.enumvalue_id == id); + + if (result) return result; + else return getEmptyEnum(); + } + + public static byShort(enums: Array, short: string): Enumerator { + let result = enums.find( + (item) => item.shorttext.toLowerCase() === short.toLowerCase() + ); + if (result) return result; + else return getEmptyEnum(); + } + + public static languages(list: Array): Array { + return Enumerations.getEnumeration(EnumKey.Language, list); + } + public static podcastGenres(list: Array): Array { + return Enumerations.getEnumeration(EnumKey.PodcastGenres, list); + } + public static podcastTypes(list: Array): Array { + return Enumerations.getEnumeration(EnumKey.PodcastTypes, list); + } + public static authors(list: Array): Array { + return Enumerations.getEnumeration(EnumKey.Authors, list); + } + public static tags(list: Array): Array { + return Enumerations.getEnumeration(EnumKey.Tags, list); + } +} + +export function getEnumFunctions(enums: Array) { + return { + getLanguage: (lang_id: number): Enumerator => { + return Enumerations.byIdOne(Enumerations.languages(enums), lang_id); + }, + + getGenre: (genre_id: number): Enumerator => { + return Enumerations.byIdOne(Enumerations.podcastGenres(enums), genre_id); + }, + + getAuthor: (autor_id: number): Enumerator => { + return Enumerations.byIdOne(Enumerations.authors(enums), autor_id); + }, + + getTag: (tag_id: number): Enumerator => { + return Enumerations.byIdOne(Enumerations.tags(enums), tag_id); + }, + getType: (type_id: number): Enumerator => { + return Enumerations.byIdOne(Enumerations.podcastTypes(enums), type_id); + }, + }; +} diff --git a/server/tagId3.ts b/backend/src/tools/Id3Tagger.ts similarity index 62% rename from server/tagId3.ts rename to backend/src/tools/Id3Tagger.ts index 1496d22..9fd5dca 100644 --- a/server/tagId3.ts +++ b/backend/src/tools/Id3Tagger.ts @@ -1,12 +1,8 @@ -import id3 from 'node-id3' -import fs from 'fs' -import { dateToIsoString, durationInSecToStr } from '~~/base/Converters'; -import IEpisode from '~~/base/types/IEpisode'; -import IPodcast from '~~/base/types/IPodcast'; -import ISerie from '~~/base/types/ISerie'; -import { nuxtPath } from './services/filesService'; - - +import * as id3 from 'node-id3' +import * as fs from 'fs' +import { dateToIsoString, durationInSecToStr } from './Converters'; +import { dataPath } from './DataFiles'; +import Episode from '../entities/Episode'; function embedCoverImage (tags: id3.Tags, file: string) : id3.Tags { const buffer = fs.readFileSync(file) @@ -22,7 +18,7 @@ function embedCoverImage (tags: id3.Tags, file: string) : id3.Tags { return tags } -function getTagsFromPodcast( episode: IEpisode ) : id3.Tags { +function getTagsFromPodcast( episode: Episode ) : id3.Tags { var tags: id3.Tags = { title: episode.title, artist: episode.creator, @@ -41,11 +37,13 @@ function getTagsFromPodcast( episode: IEpisode ) : id3.Tags { return tags } -export default function writeTags( episode: IEpisode ) : true|Error { +export default function writeTags( episode: Episode ) : true|Error { var tags = getTagsFromPodcast(episode) - tags = embedCoverImage(tags, nuxtPath(episode.image)) + const imgpath = dataPath(episode.image) + const mp3path = dataPath(episode.link) + tags = embedCoverImage(tags, imgpath) try { - const result = id3.write(tags, nuxtPath(episode.link)) + const result = id3.write(tags, mp3path) return result } catch(err) { console.log(err) diff --git a/backend/src/tools/RssGenerator.ts b/backend/src/tools/RssGenerator.ts new file mode 100644 index 0000000..23b97ea --- /dev/null +++ b/backend/src/tools/RssGenerator.ts @@ -0,0 +1,92 @@ +import { Podcast as Podclass } from 'podcast'; +import Podcast from '../entities/Podcast'; +import { serverUrl } from './Urls'; + +var testing = false; +export function setTesting() { testing = true } + + +export function generateRss( podcast: Podcast, baseUrl: string, feedSlug: string, enumFuncs: any) : string { + const feedOptions = { + title: podcast.title, + author: podcast.author, + siteUrl: podcast.link, + feedUrl: baseUrl+feedSlug+podcast.slug+".xml", + docs: baseUrl+"/"+podcast.slug, + description: podcast.description, + language: enumFuncs.getLanguage(podcast.language_id).shorttext, + copyright: podcast.copyright, + imageUrl: baseUrl+podcast.cover_file, + itunesSubtitle: podcast.subtitle, + itunesAuthor: podcast.author, + itunesSummary: podcast.summary, + itunesOwner: { name: podcast.owner_name, email: podcast.owner_email }, + itunesExplicit: podcast.explicit, + itunesCategory: [{ + text: enumFuncs.getGenre(podcast.category_id).parentCategory, + subcats: [{ + text: enumFuncs.getGenre(podcast.category_id).displaytext + }] + }], + itunesImage: baseUrl+podcast.cover_file, + itunesType: enumFuncs.getType(podcast.type_id).shorttext, + generator: "https://github.com/ccfreiburg/podkashde", + customElements: [{ + "podcast:locked" : "yes", + "podcast:guid": "bd4f4ece-3d3d-5b3d-9f24-4a081ee0d63d" + }], + customNamespaces: { + "sy": "http://purl.org/rss/1.0/modules/syndication/", + "slash": "http://purl.org/rss/1.0/modules/slash/", + "googleplay": "http://www.google.com/schemas/play-podcasts/1.0" + } + } + const feed = new Podclass(feedOptions) + + if (podcast.episodes) { + podcast.episodes.forEach( (episode) => { + feed.addItem({ + title: episode.title, + description: episode.description, + url: baseUrl+"/"+episode.slug, + guid: baseUrl+"/"+episode.slug, + author: episode.creator, + date: episode.pubdate, + enclosure: { + url: serverUrl(episode.link, baseUrl), + size: episode.rawsize, + type: "audio/mpeg" + }, + content: episode.description, + itunesAuthor: episode.creator, + //itunesKeywords: episode.keyword, + itunesExplicit: episode.explicit, + itunesSubtitle: episode.subtitle, + itunesSummary: episode.summary, + itunesDuration: episode.duration, + itunesImage: serverUrl(episode.image, baseUrl), + customElements: [ + {"itunes:block" : episode.block }, + {"googleplay:image" : [ + { + _attr: { + "href": serverUrl(episode.image, baseUrl) + } + }]}, + {"googleplay:description" : episode.description}, + {"googleplay:block" : (episode.block?"yes":"no")}, + {"googleplay:explicit" : (episode.explicit?"yes":"no")}, + {"image": [{ + "url": serverUrl(episode.image, baseUrl)}, + {"title": episode.title + }]} + ] + // itunesSeason: + // itunesEpisode: + // itunesNewFeedUrl: + }) + }) + } + + return feed.buildXml(); +} \ No newline at end of file diff --git a/backend/src/tools/Urls.ts b/backend/src/tools/Urls.ts new file mode 100644 index 0000000..f870e69 --- /dev/null +++ b/backend/src/tools/Urls.ts @@ -0,0 +1,14 @@ +export function isQualifiedUrl(target: string): Boolean { + if (target && target.length > 0) { + return target.startsWith("http://") || target.startsWith("https://"); + } + return false; + } + + export function serverUrl( episodeLink: string, baseUrl: string ) : string { + if (isQualifiedUrl(episodeLink)) + return episodeLink; + + return baseUrl + episodeLink; + } + \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..539d49c --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,16 @@ +{ + "version": "2.4.2", + "compilerOptions": { + "lib": ["es5", "es6"], + "outDir": "./dist", + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/base/Constants.ts b/base/Constants.ts index 43a7f32..1921008 100644 --- a/base/Constants.ts +++ b/base/Constants.ts @@ -1,6 +1,5 @@ export const DATA_PATH ="public"; export const ARCHIV_PATH ="archiv"; -export const PUBLIC_RESOURCES = 'public'; export const FEED_SLUG = '/s/feed/'; export const SERVER_IMG_PATH = '/s/covers/'; export const SERVER_MP3_PATH = '/s/audio/'; @@ -8,37 +7,38 @@ export const SERVER_POSTIMG_PATH = '/s/posts/'; export const SERIES_IMG_PATH = 'series'; export const REQUIRED_IMG_WIDTH = 1400; export const REQUIRED_IMG_HEIGHT = 1400; -export const UPLOAD_AP = '/api/upload'; +export const UPLOAD_AP = 'upload'; export const UPLOAD_TEMP_PATH = 'public/s/upload/'; -export const FETCHLOCAL_AP = '/api/fetchfile'; -export const FROMARCHIVE_AP = '/api/fromarchive'; -export const SERIES_AP = '/api/series'; -export const SERIE_AP = '/api/serie'; -export const PODCAST_AP = '/api/podcast'; -export const PODCASTS_AP = '/api/podcasts'; -export const EPISODES_AP = '/api/episodes'; -export const EPISODE_AP = '/api/episode'; -export const EPISODEWP_AP = '/api/episodewp'; -export const EPISODEMOVE_AP = '/api/episodemove'; -export const COUNT_AP = '/api/count'; -export const ENUMERATIONS_AP = '/api/enums'; -export const GENERATE_RSS_AP = '/api/generaterss'; +export const FETCHLOCAL_AP = 'fetchfile'; +export const FROMARCHIVE_AP = 'fromarchive'; +export const SERIES_AP = 'series'; +export const SERIE_AP = 'serie'; +export const PODCAST_AP = 'podcast'; +export const PODCASTS_AP = 'podcasts'; +export const EPISODES_AP = 'episodes'; +export const EPISODE_AP = 'episode'; +export const EPISODEWP_AP = 'episodewp'; +export const EPISODEMOVE_AP = 'episodemove'; +export const COUNT_AP = 'count'; +export const ENUMERATIONS_AP = 'enums'; +export const GENERATE_RSS_AP = 'generaterss'; export const WP_API_SLUG = 'wp-json/wp/v2/'; export const WP_PER_PAGE = '?per_page=100'; export const WP_MIN_PER_PAGE = '?per_page=1'; export const WP_HEADER_TOTAL = 'x-wp-total'; export const ROUTE_NEWPODCAST = 'newpodcast'; -export const LOGIN_AP = '/api/auth/login'; -export const LOGOUT_AP = '/api/auth/logout'; -export const REFRESH_AP = '/api/auth/refresh'; -export const AUTHUSER_AP = '/api/auth/user'; -export const USERTOKEN_AP = '/api/auth/usertoken?username=%%&type='; -export const FILES_AP = '/api/files?path='; +export const LOGIN_AP = 'auth/login'; +export const LOGOUT_AP = 'auth/logout'; +export const REFRESH_AP = 'auth/refresh'; +export const AUTHUSER_AP = 'auth/user'; +export const USERTOKEN_AP = 'auth/usertoken?username=%%&type='; +export const FILES_AP = 'files?path='; export const INVITE_TOKEN = 'invitation'; export const INVITE_TIME = '2d'; export const PWRESET_TIME = '1d'; export const SETPASS_LINK = '/admin/setpassword?token='; -export const CHECK_TOKEN_AP = '/api/auth/checktoken?token='; -export const PASSWORD_AP = '/api/auth/password' +export const CHECK_TOKEN_AP = 'auth/checktoken?token='; +export const PASSWORD_AP = 'auth/password' export const NUM_PAGINATION_LINKS = 7 -export const NUM_ITEMS_PER_PAGE = 6 \ No newline at end of file +export const NUM_ITEMS_PER_PAGE = 6 +export const TOKEN_REFRESH_TIME = 60000 \ No newline at end of file diff --git a/base/ContentFile.ts b/base/ContentFile.ts index 23ef9e3..93d2b35 100644 --- a/base/ContentFile.ts +++ b/base/ContentFile.ts @@ -18,4 +18,11 @@ export class ContentFile { public static getFilename(target: string): string { return target.split("/").slice(-1)[0]; } + + public static getMediaUrl(path: string): string { + const config = useRuntimeConfig() + const ret = (path && path.length>3?config.public.mediaBase + path:""); + return ret + } + } diff --git a/base/Converters.ts b/base/Converters.ts index d863c70..5b21772 100644 --- a/base/Converters.ts +++ b/base/Converters.ts @@ -43,7 +43,7 @@ export function strToDate(str: string): Date { return new Date(result); } -export function dateToIsoString(date: Date) { +export function dateToIsoString(date: Date) : string { return ( date.getFullYear() + "-" + @@ -53,16 +53,33 @@ export function dateToIsoString(date: Date) { ); } +export function dateToString(date: Date, locale: string) : string { + const month = date.getMonth()+1 + if (locale=="de") + return (date.getDate() + "." + month +"." +date.getFullYear()) + else if (locale=="en") + return ( month +"/" +date.getDate() + "/" +date.getFullYear()) + else + return dateToIsoString(date) +} + +export function urlFriendlyString(text: string) :string { + return saveSlugFormText(text) +} + export function saveSlugFormText(text: string, lowercase = true): string { var slug = (lowercase?text.toLowerCase():text); return slug .replace(/([:;\.,]+\s?)+/g, "_") .replace(/([!'/()*"~#@?%&\\:;,<>\*\+\|]|\DE\/EN)+/g, "") .replace(" deen", "") - .replace("ä", "ae") - .replace("ö", "oe") - .replace("ü", "ue") - .replace("ß", "ss") + .replaceAll("ä", "ae") + .replaceAll("ö", "oe") + .replaceAll("ü", "ue") + .replaceAll("Ä", "Ae") + .replaceAll("Ö", "Oe") + .replaceAll("Ü", "Ue") + .replaceAll("ß", "ss") .replace(/\s+/g, "_"); } diff --git a/base/EpisodeDetailValidation.ts b/base/EpisodeDetailValidation.ts index 6aaa5ef..1fbc7b0 100644 --- a/base/EpisodeDetailValidation.ts +++ b/base/EpisodeDetailValidation.ts @@ -1,6 +1,6 @@ import { REQUIRED_IMG_WIDTH, REQUIRED_IMG_HEIGHT } from '../base/Constants'; -import IEpisode from './types/IEpisode'; -import IValidationError from './types/IValidationError'; +import type IEpisode from './types/IEpisode'; +import type IValidationError from './types/IValidationError'; const i18naccessor = "episode.validation." diff --git a/base/Menu.ts b/base/Menu.ts deleted file mode 100644 index 5381a01..0000000 --- a/base/Menu.ts +++ /dev/null @@ -1,119 +0,0 @@ -import IMenuSection from '~~/base/types/IMenuSection'; - -export function initDefaultMenu(): Object { - var id = 0; - const menuResult = { - defaultBase: '/', - keys: [ - { - order: id++, - name: 'menu.admin', - description: 'menu.adminsub', - entries: [ - { - order: id++, - slug: 'admin/login', - name: 'menu.login', - onlyNotLoggedIn: '', - }, - { - order: id++, - slug: '#logout', - name: 'menu.logout', - onlyLoggedIn: '', - }, - { - order: id++, - slug: 'admin/setpassword', - name: 'menu.changepassword', - onlyLoggedIn: '', - }, - { - order: id++, - slug: 'admin/invitation', - name: 'menu.invitataion', - onlyAdmin: '', - }, - { - order: id++, - slug: 'admin/import', - name: 'menu.import', - onlyAdmin: '', - }, - ], - }, - { - order: id++, - name: 'menu.podcasts', - description: 'menu.podcastsub', - entries: [ - { - order: id++, - slug: 'podcasts', - name: 'menu.list', - }, - { - order: id++, - slug: 'recent', - name: 'menu.recent', - }, - { - order: id++, - slug: 'serie', - name: 'menu.series', - }, - { - order: id++, - slug: 'admin/new-podcast', - name: 'menu.new', - onlyLoggedIn: '', - }, - { - order: id++, - slug: 'admin/new-serie', - name: 'menu.newseries', - onlyLoggedIn: '', - }, - ], - }, - ] - } - return menuResult; -} - -export function getDefaultMenu( - menudata: Object, - username: string -): Object { - const loggin = username.length > 0; - const admin = username.startsWith('admin'); - const menuResult = { - defaultBase: menudata.defaultBase, - keys: menudata.keys.map((section) => { - return { - order: section.order, - name: section.name, - description: section.description, - entries: section.entries.filter( - (entry) => - (entry.hasOwnProperty('onlyLoggedIn') && loggin) || - (entry.hasOwnProperty('onlyNotLoggedIn') && !loggin) || - (entry.hasOwnProperty('onlyAdmin') && admin) || - !( - entry.hasOwnProperty('onlyLoggedIn') || - entry.hasOwnProperty('onlyNotLoggedIn') || - entry.hasOwnProperty('onlyAdmin') - ) - ), - } - }) - } - return menuResult; -} - -export async function getMenu( - url: string, -): Promise { - const menudata = await $fetch(url); - return menudata; -} diff --git a/base/PodcastDetailValidation.ts b/base/PodcastDetailValidation.ts index 0c80881..40ef1b9 100644 --- a/base/PodcastDetailValidation.ts +++ b/base/PodcastDetailValidation.ts @@ -22,7 +22,6 @@ function email(fields: Partial, errors: Array) { }); } function link(fields: Partial, key: string, errors: Array, emptyallowed: boolean = false) { - console.log(key + " " + fields[key]) if (!fields[key] || fields[key].length==0 && emptyallowed) return var re = @@ -37,7 +36,6 @@ export default function validation( imgHeight: number ): Array { var errors = [] as Array; - console.log("hallo") if ( !(imgWidth == REQUIRED_IMG_WIDTH && imgHeight == REQUIRED_IMG_HEIGHT) || !fields['cover_file'] || fields.cover_file.length < 1 diff --git a/base/RssGenerator.ts b/base/RssGenerator.ts index 7a06489..e052c67 100644 --- a/base/RssGenerator.ts +++ b/base/RssGenerator.ts @@ -1,5 +1,4 @@ import { Podcast } from 'podcast'; -import { useSettings } from '~~/composables/settingsdata'; import { FEED_SLUG, SERVER_IMG_PATH, SERVER_MP3_PATH } from './Constants'; import { ContentFile } from './ContentFile'; import IPodcast from './types/IPodcast'; diff --git a/base/SeriesDetailValidation.ts b/base/SeriesDetailValidation.ts index 2a28dca..71c3ec6 100644 --- a/base/SeriesDetailValidation.ts +++ b/base/SeriesDetailValidation.ts @@ -16,8 +16,8 @@ export default function validation( ): Array { var errors = [] as Array; if ( - !(imgWidth == REQUIRED_IMG_WIDTH && imgHeight == REQUIRED_IMG_HEIGHT) || - !fields['cover_file'] || fields.cover_file.length < 1 + (imgWidth != REQUIRED_IMG_WIDTH || imgHeight != REQUIRED_IMG_HEIGHT) && + (!fields['cover_file'] || fields.cover_file.length < 1) ) errors.push({ field: 'img', @@ -26,4 +26,4 @@ export default function validation( nonEmpty(fields, 'title', errors); nonEmpty(fields, 'slug', errors); return errors; -} +} \ No newline at end of file diff --git a/base/WpImport.ts b/base/WpImport.ts index 48ce4f9..addbec6 100644 --- a/base/WpImport.ts +++ b/base/WpImport.ts @@ -119,7 +119,9 @@ export function episodeFromWpMetadata( podcastImage, enumerations ): IEpisode { - var pubdate = strToDate(wpEpisode.meta.date_recorded); + var pubdate = strToDate((wpEpisode.meta.date_recorded.length>0?wpEpisode.meta.date_recorded:wpEpisode.date)) + if (!pubdate) + pubdate = new Date(); var image = wpEpisode.meta.cover_image; if (image.length < 1) image = podcastImage; var postimage = image; @@ -129,7 +131,7 @@ export function episodeFromWpMetadata( ) postimage = wpEpisode.episode_featured_image; var keyword = wpEpisode.tags.map((id) => enumerations.getTag(id).displaytext).join(', ') - var creator = enumerations.getAuthor(wpEpisode.speaker[0]).displaytext; + var creator = (wpEpisode.speaker?enumerations.getAuthor(wpEpisode.speaker[0]).displaytext:''); if (creator.length<1) creator = autorFromDescription(wpEpisode.excerpt.rendered) var cross_ref = verseFromDescription(wpEpisode.content.rendered) diff --git a/base/types/IFetchFileResult.ts b/base/types/IFetchFileResult.ts index 3506574..1b5cf75 100644 --- a/base/types/IFetchFileResult.ts +++ b/base/types/IFetchFileResult.ts @@ -1,5 +1,5 @@ export interface IFetchFileResult { - status: number, + statusCode: number, message: string, path?: string } \ No newline at end of file diff --git a/base/types/IPageMenuItem.ts b/base/types/IPageMenuItem.ts new file mode 100644 index 0000000..2f9eb11 --- /dev/null +++ b/base/types/IPageMenuItem.ts @@ -0,0 +1,5 @@ +export default interface IPageMenuItem { + name: string; + slug: string; + layout: string; + } \ No newline at end of file diff --git a/components/AudioFileSelector.vue b/components/AudioFileSelector.vue index aefec8a..0cecb37 100644 --- a/components/AudioFileSelector.vue +++ b/components/AudioFileSelector.vue @@ -13,11 +13,12 @@ + diff --git a/components/Menu/Link.vue b/components/Menu/Link.vue new file mode 100644 index 0000000..8fda68e --- /dev/null +++ b/components/Menu/Link.vue @@ -0,0 +1,60 @@ + + + diff --git a/components/Menu/index.vue b/components/Menu/index.vue new file mode 100644 index 0000000..fa3f458 --- /dev/null +++ b/components/Menu/index.vue @@ -0,0 +1,88 @@ + + + diff --git a/components/MessgeToast.vue b/components/MessageToast.vue similarity index 74% rename from components/MessgeToast.vue rename to components/MessageToast.vue index 11b94d6..30b5035 100644 --- a/components/MessgeToast.vue +++ b/components/MessageToast.vue @@ -8,17 +8,18 @@ >
diff --git a/components/FormDetail.vue b/components/Page/FormDetail.vue similarity index 82% rename from components/FormDetail.vue rename to components/Page/FormDetail.vue index f7cd8ee..cf22c24 100644 --- a/components/FormDetail.vue +++ b/components/Page/FormDetail.vue @@ -1,6 +1,5 @@ -import { booleanLiteral } from "@babel/types"; diff --git a/components/Page/NavBar.vue b/components/Page/NavBar.vue new file mode 100644 index 0000000..dafea4a --- /dev/null +++ b/components/Page/NavBar.vue @@ -0,0 +1,165 @@ + + + diff --git a/components/Page/SubMenu.vue b/components/Page/SubMenu.vue new file mode 100644 index 0000000..7080abf --- /dev/null +++ b/components/Page/SubMenu.vue @@ -0,0 +1,43 @@ + + diff --git a/components/PodcastDetail.vue b/components/PodcastDetail.vue index 8522fdb..0b7ae7e 100644 --- a/components/PodcastDetail.vue +++ b/components/PodcastDetail.vue @@ -1,11 +1,9 @@ diff --git a/components/SelectPodcastModal.vue b/components/SelectPodcastModal.vue index 35f525f..f0fc451 100644 --- a/components/SelectPodcastModal.vue +++ b/components/SelectPodcastModal.vue @@ -1,27 +1,27 @@ \ No newline at end of file diff --git a/components/SubMenu.vue b/components/SubMenu.vue deleted file mode 100644 index 193c465..0000000 --- a/components/SubMenu.vue +++ /dev/null @@ -1,40 +0,0 @@ - - diff --git a/composables/enumerationdata.ts b/composables/enumerationdata.ts deleted file mode 100644 index 470e493..0000000 --- a/composables/enumerationdata.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ENUMERATIONS_AP } from '~~/base/Constants'; -import Enumerations from '~~/base/Enumerations'; -import IEnumerator from '~~/base/types/IEnumerator'; - -export async function useEnumerations() { - const enums = useState>( - 'enumeration', - () => [] - ); - - const refresh = async () => { - const list = await $fetch(ENUMERATIONS_AP); - enums.value = list as Array; - }; - // if not init fetch and init - if (!Enumerations.isInitialized(enums.value)) { - await refresh(); - } - - const getLanguage = (lang_id: number): IEnumerator => { - return Enumerations.byIdOne(Enumerations.languages(enums.value), lang_id); - }; - - const getGenre = (genre_id: number): IEnumerator => { - return Enumerations.byIdOne(Enumerations.podcastGenres(enums.value), genre_id); - }; - - const getAuthor = (autor_id: number): IEnumerator => { - return Enumerations.byIdOne(Enumerations.authors(enums.value), autor_id); - }; - - const getTag = (tag_id: number): IEnumerator => { - return Enumerations.byIdOne(Enumerations.tags(enums.value), tag_id); - }; - - const getType = (type_id: number): IEnumerator => { - return Enumerations.byIdOne(Enumerations.podcastTypes(enums.value), type_id); - }; - - const languages = Enumerations.languages(enums.value); - const podcastGenres = Enumerations.podcastGenres(enums.value); - const podcastTypes = Enumerations.podcastTypes(enums.value); - const tags = Enumerations.tags(enums.value); - const authors = Enumerations.authors(enums.value); - - return { - enums, - enumerations: { - getLanguage, - getGenre, - getAuthor, - getTag, - getType, - languages, - podcastGenres, - podcastTypes, - tags, - authors - }, - refresh, - }; -} \ No newline at end of file diff --git a/composables/episodedata.ts b/composables/episodedata.ts deleted file mode 100644 index 447114a..0000000 --- a/composables/episodedata.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { EPISODES_AP, EPISODE_AP } from "~~/base/Constants"; -import IEpisode from "~~/base/types/IEpisode"; -import IPodcast from "~~/base/types/IPodcast"; -import IPostdata from "~~/base/types/IPostdata"; -import ISerie from "~~/base/types/ISerie"; - -export async function useEpisodes() { - const episodes = useState>('episodes', () => [] ) - - const refresh = async () => { - episodes.value = await $fetch(EPISODES_AP); - } - if (episodes.value.length<1) { - await refresh(); - } - return { - episodes, - refresh - } -} - -export async function useEpisode(slug:string) { - const episode = useState(slug, () => null ) - const podcast = useState("podcast-of-"+slug, () => null ) - const serie = useState("serie-of-"+slug, () => null ) - - const refresh = async () => { - episode.value = await $fetch(EPISODE_AP+"?slug="+slug) - podcast.value = episode.value.podcast; - serie.value = episode.value.serie; - } - - if (!episode.value) { - await refresh() - } - return { - episode, - podcast, - serie, - refresh, - } -} \ No newline at end of file diff --git a/composables/podcastdata.ts b/composables/podcastdata.ts deleted file mode 100644 index 4994693..0000000 --- a/composables/podcastdata.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PODCASTS_AP, PODCAST_AP } from "~~/base/Constants"; -import IEpisode from "~~/base/types/IEpisode"; -import IPodcast from "~~/base/types/IPodcast"; -import IPostdata from "~~/base/types/IPostdata"; -import ISerie from "~~/base/types/ISerie"; - -export async function usePodcasts() { - const podcasts = useState>('podcasts', () => [] ) - - const refresh = async () => { - podcasts.value = await $fetch(PODCASTS_AP); - } - // if not init fetch and init - if (podcasts.value.length<1) { - await refresh(); - } - return { - podcasts, - refresh - } -} - -export async function usePodcast(slug:string) { - const podcast = useState(slug, () => null ) - const episodes = useState>("episodes-of-"+slug, () => [] ) - const refresh = async () => { - const data: IPodcast = await $fetch(PODCAST_AP+"?slug="+slug) - podcast.value = data; - episodes.value = data.episodes - } - const remove = async () => { - const request : IPostdata = { - method: "DELETE", - body: { - id: podcast.value.id - } - } - await $fetch(PODCAST_AP, request) - podcast.value = null; - } - if (!podcast.value) { - await refresh() - } - return { - podcast, - episodes, - refresh, - remove - } -} \ No newline at end of file diff --git a/composables/seriedata.ts b/composables/seriedata.ts deleted file mode 100644 index ab3b4bf..0000000 --- a/composables/seriedata.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { SERIES_AP, SERIE_AP } from "~~/base/Constants"; -import ISerie from "~~/base/types/ISerie"; -import IPostdata from "~~/base/types/IPostdata"; -import IEpisode from "~~/base/types/IEpisode"; - - -export async function useSeries(alsoEmptySeries = true) { - const series = useState>('series_'+alsoEmptySeries, () => [] ) - - const refresh = async () => { - series.value = await $fetch(SERIES_AP+"?empty="+alsoEmptySeries); - } - // if not init fetch and init - if (series.value.length<1) { - await refresh(); - } - return { - series, - refresh - } -} - -export async function useSerie(slug:string) { - const serie = useState(slug, () => null ) - const episodes = useState>("episodes-of-"+slug, () => [] ) - - const refresh = async () => { - const data: ISerie = await $fetch(SERIE_AP+"?slug="+slug) - serie.value = data; - episodes.value = data.episodes - } - const remove = async () => { - const request : IPostdata = { - method: "DELETE", - body: { - id: serie.value.id - } - } - await $fetch(SERIE_AP, request) - serie.value = null; - } - if (!serie.value) { - await refresh() - } - return { - serie, - episodes, - refresh, - remove - } -} \ No newline at end of file diff --git a/composables/settingsdata.ts b/composables/settingsdata.ts deleted file mode 100644 index b9ce851..0000000 --- a/composables/settingsdata.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface ISettings { - baseUrl: string - defaultRoute: string - menuSource: string - skin: string - logo: string - logo_w: string - enableDarkOption: boolean, - nondefault: boolean -} - -export async function useSettings() : Promise> { - const settings = useState('settings', () => {return { - baseUrl: "http://localhost:3000", - defaultRoute: "/recent", - menuSource: "", - skin: "", - logo: "/img/logo.png", - logo_w: "/img/logo-w.png", - enableDarkOption: false, - closeOnScroll: false, - nondefault: false - }}) - if (!settings.value.nondefault) { - const set = await $fetch("/api/settings") as Partial - settings.value.nondefault = true - if (set.baseUrl) - settings.value.baseUrl = set.baseUrl - if (set.defaultRoute) - settings.value.defaultRoute = set.defaultRoute - if (set.menuSource) - settings.value.menuSource = set.menuSource - if (set.enableDarkOption) - settings.value.enableDarkOption = set.enableDarkOption - if (set.skin) - settings.value.skin = set.skin - if (set.logo) - settings.value.logo = set.logo - if (set.logo_w) - settings.value.logo_w = set.logo_w - } - return settings -} \ No newline at end of file diff --git a/composables/useAuth.ts b/composables/useAuth.ts index 5526864..9031b84 100644 --- a/composables/useAuth.ts +++ b/composables/useAuth.ts @@ -1,185 +1,162 @@ -import jwt_decode from "jwt-decode" -import { AUTHUSER_AP, LOGIN_AP, LOGOUT_AP, PASSWORD_AP, REFRESH_AP } from "../base/Constants" -import { IUser } from "../base/types/IUser" - -export default () => { - const useAuthToken = () => useState('auth_token_pk', () => null) - const useAuthUser = () => useState('auth_user_pk', () => null) - const useAuthLoading = () => useState('auth_loading_pk', () => true) - - const setToken = (newToken: string) => { - const authToken = useAuthToken() - authToken.value = newToken +import { jwtPayload } from 'jwt-payloader'; +import { LOGIN_AP, LOGOUT_AP, PASSWORD_AP, REFRESH_AP, TOKEN_REFRESH_TIME } from "../base/Constants" +import type { IUser } from "../base/types/IUser" + +var refreshTime : number = TOKEN_REFRESH_TIME +var refreshTimer:any = undefined + +interface IAuthenticationData { + user: IUser | undefined, + refresh_token: string, + access_token: string, + last_refreshed: number +} + +const loading = ref(true) +const user = ref() +const initialValue = { + user: undefined, + refresh_token: "", + access_token: "", + last_refreshed: 0 +} +var authData : IAuthenticationData = initialValue + +export default function useAuth( onetimeToken: String | undefined = undefined ) { + const { apiBase } = useRuntimeConfig().public + + const persistData = (data : IAuthenticationData) => { + if (!data.user || data.access_token.length<1) return + authData = data + user.value = data.user + if (process.client) { + localStorage.setItem('authData', JSON.stringify(authData)) + } } - const setUser = (newUser: IUser) => { - const authUser = useAuthUser() - authUser.value = newUser + const readPersistedData = () => { + if (process.client) { + const json = localStorage.getItem('authData') + authData = (json && json.startsWith("{")?JSON.parse(json):initialValue) + user.value = authData.user + } } - - const setIsAuthLoading = (value: boolean) => { - const authLoading = useAuthLoading() - authLoading.value = value + + const clearData = () => { + localStorage.clear() + authData = initialValue + user.value = undefined } - - const login = (username: string, password: string) => { - return new Promise(async (resolve, reject) => { - try { - const data = await $fetch(LOGIN_AP, { - method: 'POST', - body: { - username, - password - } - }) - - setToken(data.access_token) - setUser(data.user) - setTimer(360000) - resolve(true) - } catch (error) { - reject(error) - } - }) + + const leaveAuthenticated = ( data: IAuthenticationData ) => { + setTimer(refreshTime) + data.last_refreshed = Date.now() + persistData(data) + return data.user != undefined } - const setFirstPassword = (token: string, password: string) => { - return new Promise(async (resolve, reject) => { - try { - const data = await $fetch(PASSWORD_AP, { - method: 'POST', - body: { - token, - password - } - }) - - setToken(data.access_token) - setUser(data.user) - - resolve(true) - } catch (error) { - reject(error) - } - }) - + const login = async (username: string, password: string) => { + const data = await $fetch( apiBase + LOGIN_AP, { + method: 'POST', + body: { + username, + password + } + }) as IAuthenticationData + return leaveAuthenticated(data) } - const changePassword = (username: string, password: string, oldpassword: string) => { - return new Promise(async (resolve, reject) => { - try { - const data = await $fetch(PASSWORD_AP, { - method: 'POST', - body: { - username, - password, - oldpassword - } - }) - - setToken(data.access_token) - setUser(data.user) - - resolve(true) - } catch (error) { - reject(error) + const setFirstPassword = async (token: string, password: string) => { + const data = await useFetchApi()( PASSWORD_AP, { + method: 'POST', + body: { + token, + password } - }) - + }) as IAuthenticationData + return leaveAuthenticated(data) } - const refreshToken = () => { - return new Promise(async (resolve, reject) => { - try { - const data = await $fetch(REFRESH_AP) - setToken(data.access_token) - setUser(data.user) - - resolve(true) - } catch (error) { - setToken(null) - setUser(null) - reject(error) + const changePassword = async (username: string, password: string, oldpassword: string) => { + const data = await useFetchApi()( PASSWORD_AP, { + method: 'POST', + body: { + username, + password, + oldpassword } - }) + }) as IAuthenticationData + return leaveAuthenticated(data) } - const getUser = () => { - return new Promise(async (resolve, reject) => { - try { - const data = await $fetch(REFRESH_AP) - - setUser(data.user) - resolve(true) - } catch (error) { - reject(error) - } - }) + const refreshTheToken = async () => { + readPersistedData() + if (!hasAuthData()) return false + const myFetch = useFetchApi() + try { + const data = await myFetch( REFRESH_AP, { method: 'POST', body: authData } ) as IAuthenticationData + return leaveAuthenticated(data) + } catch { + clearData() + return false; + } } const reRefreshAccessToken = () => { - const authToken = useAuthToken() - - if (!authToken.value) { - return - } - - const jwt = jwt_decode(authToken.value) - - const newRefreshTime = jwt.exp - 60000 + readPersistedData() + if (!hasAuthData()) return false + const request = { + headers: { + 'content-Type': 'application/json', + authorization: `Bearer ${authData.refresh_token}`, + }, + }; + const jwt = jwtPayload(request) + const newRefreshTime = jwt.exp - refreshTime setTimer(newRefreshTime) } - const setTimer = (time: Number) => { - setTimeout(() => { - refreshToken().then(() => reRefreshAccessToken(), ()=>{}) - }, - time) //newRefreshTime); + const setTimer = (time: number) => { + refreshTimer = setTimeout( + refreshTheToken, + time); } - const initAuth = () => { - return new Promise(async (resolve, reject) => { - setIsAuthLoading(true) - try { - await refreshToken() - // await getUser() - - reRefreshAccessToken() - - resolve(true) - } catch (error) { - console.log(error) - reject(error) - } finally { - setIsAuthLoading(false) - } + const logout = async () => { + await useFetchApi()(LOGOUT_AP, { + method: 'POST' }) + clearData() + } + + const isSuperAdmin = () :boolean => { + return (user.value && user.value.username.toLowerCase().startsWith('admin')) } - const logout = () => { - return new Promise(async (resolve, reject) => { - try { - await useFetchApi(LOGOUT_AP, { - method: 'POST' - }) - - setToken(null) - setUser(null) - resolve() - } catch (error) { - reject(error) - } - }) + const hasAuthData = () :boolean => { + return authData.access_token!="" && authData.refresh_token!="" + } + + const haveUser = () => { + return user.value!=undefined + } + const getToken = () => { + return authData.access_token } + readPersistedData() + if (haveUser() && authData.last_refreshed>, apiSlug: string ) { + const {apiBase} = useRuntimeConfig().public + const loading = ref(!datas.value || datas.value.length==0) + + const refresh = async () => { + loading.value = true + await fetch( apiBase + apiSlug ) + .then(response => response.json()) + .then(response => { + datas.value = response + loading.value = false + }) + } + + return { + datas, + loading, + refresh + } +} + +export function useData( data: Ref, apiSlug: string, query: string ) { + + const {apiBase} = useRuntimeConfig().public + const loading = ref(!data.value) + + const refresh = async () => { + loading.value = true + await fetch( apiBase + apiSlug + query ) + .then(response => response.json()) + .then(response => { + data.value = response + loading.value = false + }) + } + + const remove = async () => { + const myFetch = useFetchApi() + await myFetch( apiSlug, { method: 'DELETE', body: { id: data.value?.id } } ) + data.value = undefined + } + + const save = async () => { + const myFetch = useFetchApi() + await myFetch( apiSlug, { method: 'POST', body: { ... data.value } } ) + } + + return { + data, + loading, + remove, + save, + refresh + } +} diff --git a/composables/useEnumerations.ts b/composables/useEnumerations.ts new file mode 100644 index 0000000..52c4163 --- /dev/null +++ b/composables/useEnumerations.ts @@ -0,0 +1,35 @@ +import { ENUMERATIONS_AP } from '~~/base/Constants'; +import Enumerations from '~~/base/Enumerations'; +import type IEnumerator from '~~/base/types/IEnumerator'; + +const enums = ref>([]) + +export function useEnumerations() { + const { refresh: dataRefresh } = useDatas( enums, ENUMERATIONS_AP ) + const loading = ref(!enums.value) + const refresh = async () => { + loading.value = true + await dataRefresh() + loading.value = false + } + if (!Enumerations.isInitialized(enums.value)) { + refresh(); + } + return { + enums, + refresh, + loading, + enumerations: { + getLanguage: (lang_id: number): IEnumerator => Enumerations.byIdOne(Enumerations.languages(enums.value), lang_id), + getGenre: (genre_id: number): IEnumerator => Enumerations.byIdOne(Enumerations.podcastGenres(enums.value), genre_id), + getAuthor: (autor_id: number): IEnumerator => Enumerations.byIdOne(Enumerations.authors(enums.value), autor_id), + getTag: (tag_id: number): IEnumerator => Enumerations.byIdOne(Enumerations.tags(enums.value), tag_id), + getType: (type_id: number): IEnumerator => Enumerations.byIdOne(Enumerations.podcastTypes(enums.value), type_id), + languages: () => Enumerations.languages(enums.value), + podcastGenres: () => Enumerations.podcastGenres(enums.value), + podcastTypes: () => Enumerations.podcastTypes(enums.value), + tags: () => Enumerations.tags(enums.value), + authors: () => Enumerations.authors(enums.value) + } + } +} \ No newline at end of file diff --git a/composables/useEpisode.ts b/composables/useEpisode.ts new file mode 100644 index 0000000..8b6aa4d --- /dev/null +++ b/composables/useEpisode.ts @@ -0,0 +1,31 @@ +import {EPISODE_AP} from '~~/base/Constants' +import type IEpisode from '~~/base/types/IEpisode' +import type IPodcast from '~~/base/types/IPodcast' +import type ISerie from '~~/base/types/ISerie' + +const episode = ref(undefined) +const podcast = ref(undefined) +const serie = ref(undefined) + +export default function useEpisode(slug:string) { + const { remove, refresh: dataRefresh } = useData( episode, EPISODE_AP, "?slug="+slug ) + const loading = ref(true) + + const refresh = async () => { + loading.value = true + await dataRefresh() + podcast.value = episode.value?.podcast as IPodcast + serie.value = episode.value?.serie as ISerie + loading.value = false + } + if (!episode.value || episode.value.slug!==slug) + refresh() + return { + episode, + podcast, + serie, + remove, + refresh, + loading + } +} \ No newline at end of file diff --git a/composables/useEpisodes.ts b/composables/useEpisodes.ts new file mode 100644 index 0000000..ebd96c4 --- /dev/null +++ b/composables/useEpisodes.ts @@ -0,0 +1,18 @@ +import {EPISODES_AP} from '~~/base/Constants' +import type IEpisode from '~~/base/types/IEpisode' +import { useDatas } from './useData' + +const episodes = ref([] as Array) + +export default function useEpisodes(directLoad = true) { + const { loading, refresh} = useDatas( episodes, EPISODES_AP ) + + if (directLoad && !episodes.value) + refresh() + + return { + episodes, + loading, + refresh + } +} diff --git a/composables/useFetchApi.ts b/composables/useFetchApi.ts index 473f1f8..98b26e6 100644 --- a/composables/useFetchApi.ts +++ b/composables/useFetchApi.ts @@ -1,12 +1,17 @@ -export default (url, options = {}) => { - const { useAuthToken } = useAuth() - - return $fetch(url, { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${useAuthToken().value}` +export default function useFetchApi() { + const { getToken } = useAuth() + const { apiBase } = useRuntimeConfig().public + return async (url: string,options: { headers?: any, body?:any, method?:any, query?:any } = {}) => { + const headers = (options.hasOwnProperty('headers')?{ ... options.headers }:{}) + const fetchOptions = { + ...options, + headers: { + ...headers, + Credentials: 'include', + Authorization: `Bearer ${getToken()}`, + } } - }) + return await $fetch( apiBase + url, fetchOptions ) + } } \ No newline at end of file diff --git a/composables/useMetaData.ts b/composables/useMetaData.ts new file mode 100644 index 0000000..e491aea --- /dev/null +++ b/composables/useMetaData.ts @@ -0,0 +1,23 @@ +const menu = ref(undefined) + +export default function useMetaData(locale: string, load = true) { + const {apiBase} = useRuntimeConfig().public + const loading = ref(!menu.value) + + const refresh = async () => { + loading.value = true + await fetch( apiBase + 'meta?locale='+locale ) + .then(response => response.json()) + .then(response => { + menu.value = response.menu + loading.value = false + }) + } + if (!menu.value && load) + refresh() + return { + menu, + loading, + refresh + } +} \ No newline at end of file diff --git a/composables/useMounted.ts b/composables/useMounted.ts new file mode 100644 index 0000000..2f4f7b5 --- /dev/null +++ b/composables/useMounted.ts @@ -0,0 +1,31 @@ +import type { RouteLocationNormalized, Router } from "vue-router"; + +export default function useMounted( refresh: Function = ()=>{}, user: Ref = ref(undefined), needsAuth: boolean = true) { + + return { + on_mounted: () => { + const router = useRouter() + if (!user.value && needsAuth) { + router.push({ + path: "/admin/login", + query: { refresh: 'true', msg: 'login.sessionexpired' }, + }) + } else + router.replace({ + ...router.currentRoute, + query: {} + })}, + on_before: () => { + const route = useRoute(); + if (route.query.refresh) refresh(); + }, + on_user_changed: (newVal: any) => { + const router = useRouter() + if (!newVal) + router.push({ + path: "/admin/login", + query: { msg: "login.sessionexpired" }, + }) + } + } +} \ No newline at end of file diff --git a/composables/usePodcast.ts b/composables/usePodcast.ts new file mode 100644 index 0000000..0a555d3 --- /dev/null +++ b/composables/usePodcast.ts @@ -0,0 +1,34 @@ +import type IEpisode from "~/base/types/IEpisode"; +import type IPodcast from "~/base/types/IPodcast"; +import { PODCAST_AP, GENERATE_RSS_AP } from "~~/base/Constants"; + + +const episodes = ref | undefined>(undefined) +const podcast = ref(undefined) + +export default function usePodcast(slug:string) { + const { remove, refresh: dataRefresh } = useData( podcast, PODCAST_AP, "?slug="+slug ) + const loading = ref(!podcast.value) + + const refresh = async () => { + loading.value = true + await dataRefresh() + episodes.value = podcast.value?.episodes as Array + delete podcast.value.episodes + loading.value = false + } + + const {generate} = useRss(slug) + + if (!podcast.value || podcast.value.slug!==slug) + refresh() + + return { + episodes, + podcast, + remove, + gernerateRss: generate, + refresh, + loading + } +} \ No newline at end of file diff --git a/composables/usePodcasts.ts b/composables/usePodcasts.ts new file mode 100644 index 0000000..a1a73c6 --- /dev/null +++ b/composables/usePodcasts.ts @@ -0,0 +1,17 @@ +import type IPodcast from "~/base/types/IPodcast"; +import { PODCASTS_AP } from "~~/base/Constants"; + +const podcasts = ref([] as Array) + +export default function usePodcasts(directLoad = true) { + const { loading, refresh} = useDatas( podcasts, PODCASTS_AP ) + + if (directLoad && (!podcasts.value || podcasts.value.length==0)) + refresh() + + return { + podcasts, + loading, + refresh + } +} \ No newline at end of file diff --git a/composables/useRss.ts b/composables/useRss.ts new file mode 100644 index 0000000..8e53083 --- /dev/null +++ b/composables/useRss.ts @@ -0,0 +1,22 @@ +import { GENERATE_RSS_AP } from "~~/base/Constants"; + + + +export default function useRss(slug:string) { + + const generate = async () => { + const {mediaBase} = useRuntimeConfig().public + const myFetch = useFetchApi() + + await myFetch( GENERATE_RSS_AP, { + query: { + slug, + mediaBase + } + }) + } + + return { + generate + } +} \ No newline at end of file diff --git a/composables/useSerie.ts b/composables/useSerie.ts new file mode 100644 index 0000000..59bc8d8 --- /dev/null +++ b/composables/useSerie.ts @@ -0,0 +1,29 @@ +import { SERIE_AP } from "~~/base/Constants"; +import type ISerie from "~~/base/types/ISerie"; +import type IEpisode from "~/base/types/IEpisode"; + +const episodes = ref | undefined>(undefined) +const serie = ref(undefined) + +export default function useSerie(slug:string) { + const { remove, refresh: dataRefresh } = useData( serie, SERIE_AP, "?slug="+slug ) + const loading = ref(!serie.value) + + const refresh = async () => { + loading.value = true + await dataRefresh() + episodes.value = serie.value?.episodes as Array + loading.value = false + } + + if (!serie.value || serie.value.slug!==slug) + refresh() + + return { + episodes, + serie, + remove, + refresh, + loading + } +} \ No newline at end of file diff --git a/composables/useSeries.ts b/composables/useSeries.ts new file mode 100644 index 0000000..2ed58e9 --- /dev/null +++ b/composables/useSeries.ts @@ -0,0 +1,18 @@ +import { SERIES_AP } from "~~/base/Constants"; +import type ISerie from "~~/base/types/ISerie"; + + +const series = ref([] as Array) + +export function useSeries(alsoEmptySeries = true, directLoad = true) { + const { loading, refresh } = useDatas( series, SERIES_AP+"?empty="+alsoEmptySeries ) + + if (directLoad && (!series.value || series.value.length==0)) + refresh() + + return { + series, + loading, + refresh + } +} \ No newline at end of file diff --git a/composables/useUploader.ts b/composables/useUploader.ts new file mode 100644 index 0000000..abbeb53 --- /dev/null +++ b/composables/useUploader.ts @@ -0,0 +1,37 @@ +import { UPLOAD_AP } from "~/base/Constants"; +import { getSaveFilename } from "~/base/Converters"; + +function getFileInFormData(fileObj: File, path: string): FormData { + const fd = new FormData(); + if (fileObj) { + fd.append("cover", fileObj, fileObj.name); + fd.append("filename", getSaveFilename(fileObj.name)); + fd.append("path", path) + } + return fd; +} + +export default function useUploader() { + return async function upload(server_path: string, fileObj: File | undefined) { + var linkToContent = ""; + var result = {}; + + if (fileObj) { + var postData = { + method: "post", + body: getFileInFormData(fileObj, server_path) + }; + const myFetch = useFetchApi() + + result = await myFetch(UPLOAD_AP, postData) as any; + } + if ((result as any).statusCode == 201 && fileObj) { + linkToContent = server_path + "/" + getSaveFilename(fileObj.name); + } + return { + link: linkToContent, + result: result, + nothingToDo: !fileObj, + } + } +} \ No newline at end of file diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000..2274d2a --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + projectId: "331z26", + env: { + appBase: 'http://localhost:3000', + apiBase: 'http://localhost:3003/api/', + mediaBase: 'http://localhost:3003' + }, + e2e: { + baseUrl: 'http://localhost:3000', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/cypress/e2e/cypress/fixtures/example.json b/cypress/e2e/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/cypress/e2e/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/e2e/cypress/support/authServices.js b/cypress/e2e/cypress/support/authServices.js new file mode 100644 index 0000000..7b646af --- /dev/null +++ b/cypress/e2e/cypress/support/authServices.js @@ -0,0 +1,31 @@ +export async function logout() { + cy.clearCookies() + cy.clearAllLocalStorage() +} + +Cypress.Commands.add('login', (password = 'AdminPassword') => { + cy.request({ url: Cypress.env('apiBase') + 'auth/login', + method: 'POST', + body: { + username: 'Admin', + password + }} + ).then((response) => { + window.localStorage.setItem('authData', JSON.stringify(response.body)) + return true + }) + }) + +Cypress.Commands.add('loginFlex', (username, password) => { + cy.request({ url: Cypress.env('apiBase') + 'auth/login', + method: 'POST', + body: { + username, + password + }} + ).then((response) => { + window.localStorage.setItem('authData', JSON.stringify(response.body)) + return true + }) + }) + \ No newline at end of file diff --git a/cypress/e2e/cypress/support/commands.js b/cypress/e2e/cypress/support/commands.js new file mode 100644 index 0000000..875aae3 --- /dev/null +++ b/cypress/e2e/cypress/support/commands.js @@ -0,0 +1,98 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +// cypress/support/commands.ts + +const nuxt_timeouts = 20000 +const interception_timeouts = 8000 + +Cypress.Commands.add('getBySel', (selector, ...args) => { + return cy.get(`[data-testid='${selector}']`, ...args) +}) + +Cypress.Commands.add('getInput', (selector, ...args) => { + return cy.get(`input[name='${selector}']`, ...args) +}) +Cypress.Commands.add('getTextArea', (selector, ...args) => { + return cy.get(`textarea[name='${selector}']`, ...args) +}) +Cypress.Commands.add('getSelect', (selector, ...args) => { + return cy.get(`select[name='${selector}']`, ...args) +}) +Cypress.Commands.add('getBySelLike', (selector, ...args) => { + return cy.get(`[data-test*=${selector}]`, ...args) +}) + +Cypress.Commands.add('waitIntercept', (interception, duration = interception_timeouts) => { + cy.wait('@'+interception, {timeout: duration}) +}) + +function setInterceptions(interceptions = []) { + for(var i=0; i { + setInterceptions(interceptions) + if (Cypress.env('NUXT_MODE') == 'development'){ + cy.intercept('GET', '/_nuxt/builds/meta/dev.json').as('nuxtDev') + cy.visit(url).wait('@nuxtDev', {timeout: nuxt_timeouts}) + } else { + cy.visit(url) + } + waitInterceptions(interceptions) +}) + +Cypress.Commands.add('clickLinkNuxtDev', (text, interceptions = []) => { + setInterceptions(interceptions) + if (Cypress.env('NUXT_MODE') == 'development'){ + cy.intercept('GET', '/_nuxt/builds/meta/dev.json').as('nuxtDev') + cy.contains(text).trigger('mouseover').wait(3).click() + cy.wait('@nuxtDev', {timeout: nuxt_timeouts}) + } else { + cy.contains(text).trigger('mouseover').wait(3).click() + } + waitInterceptions(interceptions) +}) + +Cypress.Commands.add('clickSelectorNuxtDev', (text, interceptions = []) => { + setInterceptions(interceptions) + if (Cypress.env('NUXT_MODE') == 'development'){ + cy.intercept('GET', '/_nuxt/builds/meta/dev.json').as('nuxtDev') + cy.getBySel(text).click() + cy.wait('@nuxtDev', {timeout: nuxt_timeouts}) + } else { + cy.contains(text).click() + } + waitInterceptions(interceptions) +}) \ No newline at end of file diff --git a/cypress/e2e/cypress/support/e2e.js b/cypress/e2e/cypress/support/e2e.js new file mode 100644 index 0000000..0e7290a --- /dev/null +++ b/cypress/e2e/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/cypress/e2e/cypress/support/episodeServices.js b/cypress/e2e/cypress/support/episodeServices.js new file mode 100644 index 0000000..027e31c --- /dev/null +++ b/cypress/e2e/cypress/support/episodeServices.js @@ -0,0 +1,38 @@ + +Cypress.Commands.add('deleteEpisode', (slug) => { + const authData = JSON.parse(window.localStorage.getItem('authData')) + if (!authData) throw 'authentication failed' + cy.request({ + url: Cypress.env('apiBase') + 'episode', + method: 'DELETE', + headers: { + Credentials: true, + Authorization: 'Bearer ' + authData.access_token, + }, + body: { + slug + } + }) +}) + +var currentEpisode = {} + +Cypress.Commands.add('createEpisode', (slug, podcast) => { + cy.fixture(slug).then(fixtureData => { + var currentEpisode = {...fixtureData} + currentEpisode.podcast = podcast + const authData = JSON.parse(window.localStorage.getItem('authData')) + if (!authData) throw 'authentication failed' + cy.request({ + url: Cypress.env('apiBase') + 'episode', + method: 'POST', + headers: { + Credentials: true, + Authorization: 'Bearer ' + authData.access_token, + }, + body: currentEpisode + }).then( res => { + currentEpisode.id = res.body.id + }) + }) +}) \ No newline at end of file diff --git a/cypress/e2e/cypress/support/podcastServices.js b/cypress/e2e/cypress/support/podcastServices.js new file mode 100644 index 0000000..a25d436 --- /dev/null +++ b/cypress/e2e/cypress/support/podcastServices.js @@ -0,0 +1,39 @@ + +Cypress.Commands.add('deletePodcast', (slug) => { + const authData = JSON.parse(window.localStorage.getItem('authData')) + if (!authData) throw 'authentication failed' + cy.request({ + url: Cypress.env('apiBase') + 'podcast', + method: 'DELETE', + headers: { + Credentials: true, + Authorization: 'Bearer ' + authData.access_token, + }, + body: { + slug + } + }) +}) + +Cypress.Commands.add('createPodcast', (slug) => { + cy.fixture(slug).then(fixtureData => { + const authData = JSON.parse(window.localStorage.getItem('authData')) + if (!authData) throw 'authentication failed' + cy.request({ + url: Cypress.env('apiBase') + 'podcast', + method: 'POST', + headers: { + Credentials: true, + Authorization: 'Bearer ' + authData.access_token, + }, + body: fixtureData + }).then( res => { + cy.request({ + url: Cypress.env('apiBase') + 'podcast?slug=' + slug, + method: 'GET' + }).then( res => { + return res.body + }) + }) + }) +}) \ No newline at end of file diff --git a/cypress/e2e/cypress/support/serieServices.js b/cypress/e2e/cypress/support/serieServices.js new file mode 100644 index 0000000..b2dd3b1 --- /dev/null +++ b/cypress/e2e/cypress/support/serieServices.js @@ -0,0 +1,40 @@ + +Cypress.Commands.add('deleteSerie', (slug) => { + const authData = JSON.parse(window.localStorage.getItem('authData')) + if (!authData) throw 'authentication failed' + cy.request({ + url: Cypress.env('apiBase') + 'serie', + method: 'DELETE', + headers: { + Credentials: true, + Authorization: 'Bearer ' + authData.access_token, + }, + body: { + slug + } + }) +}) + +Cypress.Commands.add('createSerie', (slug) => { + cy.fixture(slug).then(fixtureData => { + const authData = JSON.parse(window.localStorage.getItem('authData')) + if (!authData) throw 'authentication failed' + cy.request({ + url: Cypress.env('apiBase') + 'series', + method: 'POST', + headers: { + Credentials: true, + Authorization: 'Bearer ' + authData.access_token, + }, + body: [ fixtureData ] + }).then( res => { + cy.request({ + url: Cypress.env('apiBase') + 'serie', + method: 'GET', + body: { id: res.body.id } + }).then( res => { + return res.body + }) + }) + }) +}) \ No newline at end of file diff --git a/cypress/e2e/episode-new.cy.js b/cypress/e2e/episode-new.cy.js new file mode 100644 index 0000000..6f4927a --- /dev/null +++ b/cypress/e2e/episode-new.cy.js @@ -0,0 +1,64 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/podcastServices.js' +import './cypress/support/episodeServices.js' + +describe('', () => { + const podcast_slug = "a_new_podcast" + before('', () => { + cy.viewport(1000, 1000) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('', () => { + cy.intercept('GET','meta').as('meta') + cy.login().then(() => { + cy.createPodcast(podcast_slug) + }) + cy.visitNuxtDev('/admin/podcast/'+podcast_slug+'/new-episode', [ + { + method: 'GET', + url: '*meta?*', + id: 'meta' + } + ]) + }) + afterEach('', () => { + cy.deletePodcast(podcast_slug) + }) + it('does display new episode page', () => { + cy.contains('h1','Neue Folge') + cy.contains('A New Podcast') + }) + it('When submitting empty form, displays all validation errrors', () => { + cy.getInput('title').type('{Enter}') + cy.contains('Bitte ein Podcast Titelbild mit 1400x1400 Pixel auswählen') + cy.contains('Bitte einen Titel eingeben') + cy.contains('Bitte einen Autor eingeben') + cy.contains('Bitte einen Slug eingeben, der noich nicht verwendet wurd') + }) + it('Saves correct Episode with MP3 Tags into Podcast', ()=>{ + cy.deleteEpisode('test_slug') + cy.intercept('POST','upload').as('upload') + cy.intercept('POST','episode').as('episode') + cy.intercept('GET','*generaterss?*').as('rss') + cy.intercept('GET','*enum?*').as('enum') + cy.intercept('GET','*podcast?*').as('podcast') + cy.get('input[name="audioFileInput"]').selectFile('cypress/fixtures/1test.mp3', { + action: "select", + force: true, + }); + cy.getInput('creator').value = 'Alexander Röhm' + cy.getInput('slug').type('{selectall}test_slug') + cy.getInput('title').type('{Enter}') + cy.wait(8) + cy.waitIntercept('upload') + cy.waitIntercept('upload') + cy.waitIntercept('episode') + cy.waitIntercept('rss') + cy.waitIntercept('enum') + cy.contains('Podcast Folgen') + cy.contains('diesem Podcast') + cy.contains('Welchen Tod willst du sterben? Welches Leben willst du leben?') + cy.deleteEpisode('test_slug') + }) +}) \ No newline at end of file diff --git a/cypress/e2e/episode-submenu-actions.cy.js b/cypress/e2e/episode-submenu-actions.cy.js new file mode 100644 index 0000000..e9abbb8 --- /dev/null +++ b/cypress/e2e/episode-submenu-actions.cy.js @@ -0,0 +1,54 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/podcastServices.js' +import './cypress/support/episodeServices.js' + +describe('', () => { + const podcast_slug = "a_new_podcast" + const episode_slug = "a_new_episode" + const second_podcast_slug = "a_second_podcast" + before('', () => { + cy.viewport(1000, 1000) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('', () => { + cy.login().then(() => { + cy.createPodcast(podcast_slug).then(podcast => { + cy.createEpisode(episode_slug, podcast) + cy.createPodcast(second_podcast_slug) + }) + }) + }) + afterEach('', () => { + cy.deleteEpisode(episode_slug) + cy.deletePodcast(podcast_slug) + cy.deletePodcast(second_podcast_slug) + }) + it('Change Podcast for Episode', () => { + cy.visitNuxtDev(episode_slug, [ + { + method: 'GET', + url: 'podcasts', + id: 'podcasts' + }, + { + method: 'GET', + url: '*episode?*', + id: 'episode' + }, + { + method: 'GET', + url: '*meta?*', + id: 'meta' + } + ]) + cy.wait(3) + cy.getBySel('#change').click({ force: true }) + cy.getSelect('select_podcast').select(1) + cy.intercept('POST','episodemove', (req) => { + req.reply( { statusCode: 201 }) + }).as('move') + cy.getBySel('submit').click({ force: true }) + cy.wait('@move') + }) + }) \ No newline at end of file diff --git a/cypress/e2e/home-and-menu.cy.js b/cypress/e2e/home-and-menu.cy.js new file mode 100644 index 0000000..bc90118 --- /dev/null +++ b/cypress/e2e/home-and-menu.cy.js @@ -0,0 +1,25 @@ +import './cypress/support/e2e.js' + +describe('template spec', () => { + beforeEach('Visit Home', () => { + cy.intercept('GET', '/api/episodes').as('episodes') + cy.visitNuxtDev('/en') + cy.waitIntercept('episodes') + }) + it('Shows Title', () => { + cy.contains('h1','Recent Episodes') + }); + it('Menu Sandwich Button clicked shows menu with podcasts link', () => { + cy.getBySel('NavBar.clickableElement').click({ force: true }) + cy.contains('Podcasts').should('have.attr', 'href') + cy.getBySel('NavBar.clickableElement').click({ force: true }) + cy.contains('Podcasts').should('have.length', 0) + }); + it('Language Button clicked shows Deutsch', () => { + cy.wait(2000) + cy.getBySel('show-locale-dropdown').click({ force: true }) + cy.getBySel('switch-locale-de').click({ force: true }) + cy.contains('Deutsch').click({ force: true }) + cy.contains('h1','Kürzlich erschienen') + }); +}) \ No newline at end of file diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js new file mode 100644 index 0000000..aedbb89 --- /dev/null +++ b/cypress/e2e/login.cy.js @@ -0,0 +1,47 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import { logout } from './cypress/support/authServices.js' + +describe('', () => { + before('', () => { + cy.viewport(1000, 1000) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('Visit Home', () => { + logout() + cy.visitNuxtDev('/admin/login') + cy.intercept('POST', '/api/auth/login').as('apilogin') + }) + it('Shows Title', () => { + cy.contains('h1','Anmelden') + }) + it('Empty Passwort', () => { + cy.getInput('user').type('Fred{Enter}') + cy.waitIntercept('apilogin').then((interception) => { + expect(interception.response.statusCode).to.eq(400) + expect(interception.response.body).to.not.have.property('access_token') + }) + cy.contains('Passwort oder Nutzername falsch') + }) + it('Empty User', () => { + cy.getInput('password').type('Fred') + cy.getBySel('submit').click() + cy.waitIntercept('apilogin').then((interception) => { + expect(interception.response.statusCode).to.eq(400) + expect(interception.response.body).to.not.have.property('access_token') + }) + cy.contains('Passwort oder Nutzername falsch') + }) + it('Login', () => { + cy.getInput('user').clear().type('Admin') + cy.getInput('password').clear().type('AdminPassword') + cy.getBySel('submit').click() + cy.waitIntercept('apilogin').then((interception) => { + expect(interception.response.statusCode).to.eq(200) + expect(interception.response.body).to.have.property('access_token') + }) + cy.contains('Podcasts') + cy.contains('Neuer Podcast') + }) + +}) \ No newline at end of file diff --git a/cypress/e2e/password.cy.js b/cypress/e2e/password.cy.js new file mode 100644 index 0000000..0dced28 --- /dev/null +++ b/cypress/e2e/password.cy.js @@ -0,0 +1,61 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import { logout } from './cypress/support/authServices.js' + +describe('', () => { + before('', () => { + cy.viewport(1000, 1000) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('Visit Home', () => { + cy.login() + cy.wait(3) + }) + it('Cange Password displays error on two different passwords', () => { + cy.visitNuxtDev('/admin/setpassword' ) + cy.contains('h1','Neues Passwort') + cy.getInput('passwordOld').type('Admin') + cy.getInput('password1').type('password') + cy.getInput('password2').type('password2{Enter}') + cy.contains('Die Passwörter stimmen nicht überein') + }) + it('Cange Password password too short', () => { + cy.visitNuxtDev('/admin/setpassword' ) + cy.contains('h1','Neues Passwort') + cy.getInput('passwordOld').type('Admin') + cy.getInput('password1').type('pass') + cy.getInput('password2').type('pass{Enter}') + cy.contains('Das Passwort soll mindestens 8 Zeichen haben') + }) + it('Cange Password works', () => { + cy.visitNuxtDev('/admin/setpassword' ) + cy.contains('h1','Neues Passwort') + cy.getInput('passwordOld').type('AdminPassword') + cy.getInput('password1').type('password') + cy.getInput('password2').type('password{Enter}') + logout() + cy.login('password') + cy.visitNuxtDev('/admin/setpassword' ) + cy.wait(3) + cy.getInput('passwordOld').type('password') + cy.getInput('password1').type('AdminPassword') + cy.getInput('password2').type('AdminPassword{Enter}') + }) + it('Invite User', () => { + cy.visitNuxtDev('/admin/invitation') + cy.contains('Nutzer einladen') + cy.intercept('GET', '/api/auth/usertoken?*').as('usertoken') + cy.getInput('username').type('regularjoe{Enter}') + cy.waitIntercept('usertoken') + cy.wait(2) + cy.getTextArea('invitelinktext').invoke('val') + .then(actualValue => { + cy.visitNuxtDev(actualValue) + cy.contains('h1','Neues Passwort') + cy.getInput('password1').type('AdminPassword') + cy.getInput('password2').type('AdminPassword{Enter}') + cy.wait(3) + cy.loginFlex('regularjoe','AdminPassword') + }) +}) +}) \ No newline at end of file diff --git a/cypress/e2e/podcast-edit.cy.js b/cypress/e2e/podcast-edit.cy.js new file mode 100644 index 0000000..a207780 --- /dev/null +++ b/cypress/e2e/podcast-edit.cy.js @@ -0,0 +1,59 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/podcastServices.js' + +describe('', () => { + const slug = "a_new_podcast" + before('', () => { + cy.viewport(1000, 1400) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('Edit Podcast Page', () => { + cy.intercept('GET','enums').as('enums') + cy.login().then(() => { + cy.createPodcast(slug).then( podcast => { + cy.wait(3) + cy.visitNuxtDev('/admin/podcast/'+podcast.slug ) + }) + }) + cy.waitIntercept('enums') + }) + afterEach('', () => { + cy.deletePodcast(slug) + }) + it('Shows Title', () => { + cy.contains('h1','Podcast bearbeiten') + }) + it('Change Author', () => { + cy.intercept('GET','*generaterss?*').as('rss') + cy.getInput('author').clear().type('fritzile{Enter}') + cy.location().should(loc => { + expect(loc.pathname).to.equal('/podcast/'+slug) + }) + cy.waitIntercept('rss') + cy.contains('fritzile') + }) + it('Empty Author creates validation error', () => { + cy.getInput('author').clear().type('{Enter}') + cy.contains('Bitte einen Autor eingeben') + }) + it('Change Image', () => { + cy.intercept('GET','*generaterss?*').as('rss') + cy.intercept('GET','count*').as('count') + cy.intercept('POST','upload').as('upload') + cy.intercept('POST','podcast').as('podcast') + cy.get('input[type=file]').selectFile('cypress/fixtures/pod-cover3.jpg', { + action: "select", + force: true, + }); + cy.getInput('author').type('{Enter}').wait(3) + cy.waitIntercept('count') + cy.waitIntercept('upload',12000) + cy.waitIntercept('podcast') + cy.waitIntercept('rss') + cy.location({timeout: 10000}).should(loc => { + expect(loc.pathname).to.equal('/podcast/'+slug) + }) + cy.getBySel('content-area').find('img').should('have.attr', 'src').should('include','pod-cover3.jpg') + }) +}) \ No newline at end of file diff --git a/cypress/e2e/podcast-new.cy.js b/cypress/e2e/podcast-new.cy.js new file mode 100644 index 0000000..2d39468 --- /dev/null +++ b/cypress/e2e/podcast-new.cy.js @@ -0,0 +1,80 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/podcastServices.js' + +describe('', () => { + before('', () => { + }) + beforeEach('Visit Home', () => { + cy.login() + cy.visitNuxtDev('/admin/new-podcast', [ + { + method: 'GET', + url: '*meta?*', + id: 'meta' + } + + ]) + }) + + it('New Podcast Page', () => { + cy.contains('h1','Neu') + }) + it('Shows All Errors on Empty Form submitted', () => { + cy.getInput('title').type('{Enter}') + cy.contains('Bitte ein Podcast Titelbild') + cy.contains('Bitte einen Titel eingeben') + cy.contains('Bitte einen Autor eingeben') + cy.contains('Bitte einen eindeutingen Slug eingeben') + cy.contains('Bitte den Namen der verantwortlichen Person eingeben') + cy.contains('Bitte eine Sprache wählen') + cy.contains('Bitten eine Podcastkategorie wählen') + cy.contains('Bitte eine Art der Veröffentlichung wählen') + cy.contains('Bitte die Email der verantwortlichen Person eingeben') + }) + it('Only cover image missing', () => { + const slug = "a_new_podcast-podcast-new-3" + cy.getInput('title').type('title') + cy.getInput('author').type('author') + cy.getInput('slug').clear().type(slug) + cy.getSelect('language').select(1) + cy.getSelect('category').select(1) + cy.getSelect('type').select(1) + cy.getInput('owner_name').type('owner_name') + cy.getInput('owner_email').type('owner@ema.il{Enter}') + cy.contains('Bitte ein Podcast Titelbild') + cy.contains('Bitte einen').should('not.exist') + }) + it('Form without Errors saving', () => { + const slug = "a_new_podcast-podcast-new-4" + cy.intercept('GET','*generaterss?*').as('rss') + cy.intercept('POST','upload').as('upload') + cy.intercept('POST','podcast').as('podcast') + cy.get('input[type=file]').selectFile('cypress/fixtures/pod-cover2.jpg', { + action: "select", + force: true, + }); + cy.getInput('title').type('title') + cy.getInput('author').type('author') + cy.getInput('slug').clear().type(slug) + cy.getSelect('language').select(1) + cy.getSelect('category').select(1) + cy.getSelect('type').select(1) + cy.getInput('owner_name').type('owner_name') + cy.getInput('owner_email').type('owner@ema.il{Enter}').wait(3) + cy.waitIntercept('upload',12000) + cy.waitIntercept('podcast') + cy.waitIntercept('rss') + cy.getBySel("podcast."+slug); + cy.deletePodcast(slug) + }) + it('Displays Error when slug already exists', () => { + const slug = "a_new_podcast" + cy.createPodcast(slug) + cy.intercept('GET','count*').as('count') + cy.getInput('slug').clear().type(slug+"{Enter}") + cy.waitIntercept('count') + cy.contains('Bitte einen eindeutingen Slug') + cy.deletePodcast(slug) + }) +}) \ No newline at end of file diff --git a/cypress/e2e/podcast-submenu-actions.cy.js b/cypress/e2e/podcast-submenu-actions.cy.js new file mode 100644 index 0000000..79b0d32 --- /dev/null +++ b/cypress/e2e/podcast-submenu-actions.cy.js @@ -0,0 +1,44 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/podcastServices.js' + +describe('', () => { + const slug = "a_new_podcast" + before('', () => { + cy.viewport(1000, 1000) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('', () => { + cy.login().then(()=>{ + cy.createPodcast(slug).then(podcast => { + cy.visitNuxtDev('/podcast/'+podcast.slug) + }) + }) + }) + afterEach('', () => { + cy.deletePodcast(slug) + }) + it('does display podcast detail', () => { + cy.contains('h1','Podcast') + cy.contains('A New Podcast') + }) + it('deletes podcast, when click delete', () => { + cy.intercept('DELETE','podcast').as('podcast') + cy.getBySel('#delete').click({ force: true }) + cy.waitIntercept('podcast') + cy.location().should(loc => { + expect(loc.pathname).to.equal('/podcasts') + }) + cy.contains('A New Podcast').should('not.exist') + }) + it('opens add episode dialog, when click add', () => { + const url = '/admin/podcast/'+slug+'/new-episode' + cy.getBySel(url).wait(5).click().wait(3) + cy.location({timeout: 12000}).should(loc => { + expect(loc.pathname).to.equal(url) + }) + cy.contains('h1','Neue Folge') + cy.contains('für') + cy.contains('A New Podcast') + }) +}) \ No newline at end of file diff --git a/cypress/e2e/serie-edit.cy.js b/cypress/e2e/serie-edit.cy.js new file mode 100644 index 0000000..9890a01 --- /dev/null +++ b/cypress/e2e/serie-edit.cy.js @@ -0,0 +1,49 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/serieServices.js' + +describe('', () => { + const slug = "a_new_serie" + before('', () => { + cy.viewport(1000, 1400) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('Edit serie Page', () => { + cy.login().then( ()=> { + cy.createSerie(slug).then( ser => { + cy.visitNuxtDev('/admin/serie/'+slug) + }) + }) + cy.contains('Serie bearbeiten') + }) + afterEach('', () => { + cy.deleteSerie(slug) + }) + it('Shows Title', () => { + cy.contains('h1','Serie bearbeiten') + }) + it('Change Title', () => { + cy.getInput('title').clear().type('fritzile{Enter}') + cy.wait(3) + cy.contains('fritzile') + }) + it('Empty Author creates validation error', () => { + cy.getInput('title').clear().type('{Enter}') + cy.contains('Bitte einen Titel') + }) + it('Change Image', () => { + cy.intercept('POST','upload').as('upload') + cy.intercept('POST','series').as('series') + cy.get('input[type=file]').selectFile('cypress/fixtures/serie-cover4.jpg', { + action: "select", + force: true, + }); + cy.getInput('title').type('{Enter}') + cy.waitIntercept('upload',12000) + cy.waitIntercept('series') + cy.location({timeout: 9000}).should(loc => { + expect(loc.pathname).to.equal('/serie/'+slug) + }) + cy.getBySel('content-area').find('img').should('have.attr', 'src').should('include','serie-cover4.jpg') + }) +}) \ No newline at end of file diff --git a/cypress/e2e/serie-new.cy.js b/cypress/e2e/serie-new.cy.js new file mode 100644 index 0000000..addf465 --- /dev/null +++ b/cypress/e2e/serie-new.cy.js @@ -0,0 +1,55 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/serieServices.js' + +describe('', () => { + before('', () => { + cy.viewport(1000, 1400) + }) + beforeEach('Visit Home', () => { + cy.login() + cy.visitNuxtDev('/admin/new-serie', [ + { + method: 'GET', + url: '*meta?*', + id: 'meta' + } + + ]) + }) + it('New Serie Page', () => { + cy.contains('h1','Neue ') + }) + it('Shows All Errors on Empty Form submitted', () => { + cy.getInput('title').type('{Enter}') + cy.contains('Bitte ein Podcast Titelbild') + cy.contains('Bitte einen Titel eingeben') + cy.contains('Bitte einen eindeutingen Slug eingeben') + }) + it('Only cover image missing', () => { + cy.getInput('title').type('title{Enter}') + cy.contains('Bitte ein Podcast Titelbild') + cy.contains('Bitte einen').should('not.exist') + }) + it('Form without Errors saving', () => { + cy.intercept('POST','series').as('series') + cy.intercept('POST','upload').as('upload') + cy.get('input[type=file]').selectFile('cypress/fixtures/serie-cover3.jpg', { + action: "select", + force: true, + }); + cy.getInput('title').type('title{Enter}') + cy.waitIntercept('upload',12000) + cy.waitIntercept('series') + cy.getBySel("serie.title") + cy.deleteSerie('title') + }) + it('Displays Error when slug already exists', () => { + const slug = "a_new_podcast" + cy.createSerie(slug) + cy.intercept('GET','count*').as('count') + cy.getInput('slug').type(slug+"{Enter}").wait('@count') + cy.contains('Bitte einen eindeutingen Slug') + cy.deleteSerie(slug) + }) +}) \ No newline at end of file diff --git a/cypress/e2e/serie-submenu-actions.cy.js b/cypress/e2e/serie-submenu-actions.cy.js new file mode 100644 index 0000000..e909574 --- /dev/null +++ b/cypress/e2e/serie-submenu-actions.cy.js @@ -0,0 +1,42 @@ +import './cypress/support/e2e.js' +import './cypress/support/authServices.js' +import './cypress/support/serieServices.js' + +describe('', () => { + const slug = "a_new_serie" + before('', () => { + cy.viewport(1000, 1000) + //cy.intercept('GET', '/api/meta?locale=en', async (req) => req.reply( { fixture: 'meta-en.json' }) ) + }) + beforeEach('', () => { + cy.login() + cy.visitNuxtDev('/series', [ + { + method: 'GET', + url: '*meta?*', + id: 'meta' + } + + ]) + cy.createSerie(slug) + cy.visitNuxtDev('/serie/'+slug, [ + { + method: 'GET', + url: '*meta?*', + id: 'meta' + } + + ]) + }) + afterEach('', () => { + cy.deleteSerie(slug) + }) + it('does display serie detail', () => { + cy.contains('h1','Serie') + cy.contains('A new Serie') + }) + it('deletes serie, when click delete', () => { + cy.clickLinkNuxtDev('Löschen') + cy.contains('A New serie').should('not.exist') + }) +}) \ No newline at end of file diff --git a/cypress/fixtures/1test.mp3 b/cypress/fixtures/1test.mp3 new file mode 100644 index 0000000..47bb314 Binary files /dev/null and b/cypress/fixtures/1test.mp3 differ diff --git a/cypress/fixtures/a_new_episode.json b/cypress/fixtures/a_new_episode.json new file mode 100644 index 0000000..61e9b69 --- /dev/null +++ b/cypress/fixtures/a_new_episode.json @@ -0,0 +1,28 @@ +{ + "title": "A new Episode", + "link": "/test/test.mp3", + "slug": "a_new_episode", + "pubdate": "2024-02-03T00:00:00.000Z", + "creator": "Samuel Garrard", + "description": "", + "subtitle": "", + "keyword": "", + "summary": "sonntag I serie", + "image": "/test/img/cover.jpg", + "postimage": "", + "block": false, + "explicit": false, + "duration": 2954, + "rawsize": 95256493, + "state": -1, + "draft": false, + "video_link": null, + "cross_ref": null, + "external_id": -1, + "ext_series_id": -1, + "ext_podcast_id": -1, + "lastbuild": "", + "deletedAt": null, + "podcast": null, + "serie": null +} \ No newline at end of file diff --git a/cypress/fixtures/a_new_podcast.json b/cypress/fixtures/a_new_podcast.json new file mode 100644 index 0000000..2e92974 --- /dev/null +++ b/cypress/fixtures/a_new_podcast.json @@ -0,0 +1,29 @@ +{ + "cover_file": "/test/img/cover.jpg", + "title": "A New Podcast", + "slug": "a_new_podcast", + "subtitle": "It also has a Sub-Title", + "author": "Podcashde Testing", + "summary": "This record is created by automated testing ", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fringilla phasellus faucibus scelerisque eleifend donec. Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Lorem ipsum dolor sit amet. Aenean vel elit scelerisque mauris pellentesque pulvinar. Pretium lectus quam id leo in vitae turpis massa sed. Posuere ac ut consequat semper viverra nam. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. In cursus turpis massa tincidunt dui ut. Elit eget gravida cum sociis natoque penatibus et magnis dis. Porta lorem mollis aliquam ut porttitor. Gravida cum sociis natoque penatibus et. Sed felis eget velit aliquet sagittis id consectetur purus ut.\n\nTellus molestie nunc non blandit. Nibh sit amet commodo nulla. Dui ut ornare lectus sit amet est. Cursus eget nunc scelerisque viverra. Imperdiet nulla malesuada pellentesque elit eget. Quis eleifend quam adipiscing vitae proin sagittis nisl. Arcu bibendum at varius vel pharetra vel turpis. Nunc aliquet bibendum enim facilisis gravida. Aliquet enim tortor at auctor urna nunc. Viverra maecenas accumsan lacus vel facilisis volutpat est. Id nibh tortor id aliquet lectus proin. Sollicitudin tempor id eu nisl nunc mi ipsum faucibus vitae. Nulla at volutpat diam ut venenatis tellus in. Velit aliquet sagittis id consectetur purus ut. Purus in mollis nunc sed id semper risus. Feugiat nisl pretium fusce id velit ut tortor pretium.", + "language_id": 1, + "category_id": 25, + "type_id": 1, + "explicit": false, + "link": "https://github.com/ccfreiburg/podkashde", + "copyright": "The Podkashde Team", + "owner_name": "Me Myself", + "owner_email": "me@myself.me", + "lastbuild": "", + "state": -1, + "external_id": -1, + "draft": false, + "apple_url": "https://github.com/ccfreiburg/podkashde", + "spotify_url": "https://github.com/ccfreiburg/podkashde", + "google_url": "https://github.com/ccfreiburg/podkashde", + "stitcher_url": "https://github.com/ccfreiburg/podkashde", + "createdAt": "2024-02-07T09:13:27.000Z", + "updatedAt": "2024-02-07T09:13:27.000Z", + "deletedAt": null, + "episodes": [] +} diff --git a/cypress/fixtures/a_new_serie.json b/cypress/fixtures/a_new_serie.json new file mode 100644 index 0000000..9e583d3 --- /dev/null +++ b/cypress/fixtures/a_new_serie.json @@ -0,0 +1,13 @@ +{ + "cover_file": "/test/img/cover.jpg", + "title": "A new Serie", + "slug": "a_new_serie", + "subtitle": "the sub title", + "description": "", + "state": 16, + "draft": false, + "lastbuild": "", + "external_id": -1, + "firstEpisode": null, + "lastEpisode": null +} diff --git a/cypress/fixtures/a_second_podcast.json b/cypress/fixtures/a_second_podcast.json new file mode 100644 index 0000000..b889e1c --- /dev/null +++ b/cypress/fixtures/a_second_podcast.json @@ -0,0 +1,29 @@ +{ + "cover_file": "/test/img/cover.jpg", + "title": "A second Podcast", + "slug": "a_second_podcast", + "subtitle": "It also has a Sub-Title", + "author": "Podcashde Testing", + "summary": "This record is created by automated testing ", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fringilla phasellus faucibus scelerisque eleifend donec. Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Lorem ipsum dolor sit amet. Aenean vel elit scelerisque mauris pellentesque pulvinar. Pretium lectus quam id leo in vitae turpis massa sed. Posuere ac ut consequat semper viverra nam. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. In cursus turpis massa tincidunt dui ut. Elit eget gravida cum sociis natoque penatibus et magnis dis. Porta lorem mollis aliquam ut porttitor. Gravida cum sociis natoque penatibus et. Sed felis eget velit aliquet sagittis id consectetur purus ut.\n\nTellus molestie nunc non blandit. Nibh sit amet commodo nulla. Dui ut ornare lectus sit amet est. Cursus eget nunc scelerisque viverra. Imperdiet nulla malesuada pellentesque elit eget. Quis eleifend quam adipiscing vitae proin sagittis nisl. Arcu bibendum at varius vel pharetra vel turpis. Nunc aliquet bibendum enim facilisis gravida. Aliquet enim tortor at auctor urna nunc. Viverra maecenas accumsan lacus vel facilisis volutpat est. Id nibh tortor id aliquet lectus proin. Sollicitudin tempor id eu nisl nunc mi ipsum faucibus vitae. Nulla at volutpat diam ut venenatis tellus in. Velit aliquet sagittis id consectetur purus ut. Purus in mollis nunc sed id semper risus. Feugiat nisl pretium fusce id velit ut tortor pretium.", + "language_id": 1, + "category_id": 25, + "type_id": 1, + "explicit": false, + "link": "https://github.com/ccfreiburg/podkashde", + "copyright": "The Podkashde Team", + "owner_name": "Me Myself", + "owner_email": "me@myself.me", + "lastbuild": "", + "state": -1, + "external_id": -1, + "draft": false, + "apple_url": "https://github.com/ccfreiburg/podkashde", + "spotify_url": "https://github.com/ccfreiburg/podkashde", + "google_url": "https://github.com/ccfreiburg/podkashde", + "stitcher_url": "https://github.com/ccfreiburg/podkashde", + "createdAt": "2024-02-07T09:13:27.000Z", + "updatedAt": "2024-02-07T09:13:27.000Z", + "deletedAt": null, + "episodes": [] +} diff --git a/cypress/fixtures/dev.json b/cypress/fixtures/dev.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/cypress/fixtures/dev.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/fixtures/meta-de.json b/cypress/fixtures/meta-de.json new file mode 100644 index 0000000..c6d0222 --- /dev/null +++ b/cypress/fixtures/meta-de.json @@ -0,0 +1,65 @@ +{ + "menu": [{ + "Description": "Höre aktuelle und zuletzt erschienene Folgen", + "Name": "Hör rein", + "Replaces": "Hör rein", + "link": "/recent", + "menu_items": [ + { + "Name": "Aktuelle Folgen", + "locale": "de", + "link": "/recent", + "local": true + }, + { + "Name": "Aktuelle Serien", + "locale": "de", + "link": "/series", + "local": true + }, + { + "Name": "Podcasts", + "locale": "de", + "link": "/podcasts", + "local": true + } + ] + }, + { + "Description": "Podcasts, Serien und Folgen verwalten ", + "Name": "Administrator", + "admin": true, + "link": "/admin", + "menu_items": [ + { + "Name": "Andministrator Login", + "locale": "de", + "loggedin": false, + "link": "/admin/login", + "local": true + }, + { + "Name": "Passwort ändern", + "loggedin": true, + "locale": "de", + "link": "/admin/setpassword", + "local": true + }, + { + "Name": "Benutzer einladen", + "locale": "de", + "loggedin": true, + "link": "/admin/invitation", + "local": true + }, + { + "Name": "Abmelden", + "locale": "de", + "loggedin": true, + "link": "#logout", + "local": true + } + ] + } + ] +} \ No newline at end of file diff --git a/cypress/fixtures/meta-en.json b/cypress/fixtures/meta-en.json new file mode 100644 index 0000000..fa24ad7 --- /dev/null +++ b/cypress/fixtures/meta-en.json @@ -0,0 +1,66 @@ +{ + "menu": [ + { + "Description": "Listen to our content", + "Name": "Listen", + "Replaces": "Listen", + "link": "/recent", + "menu_items": [ + { + "Name": "Recent Episodes", + "locale": "en", + "link": "/recent", + "local": true + }, + { + "Name": "Current Series", + "locale": "en", + "link": "/series", + "local": true + }, + { + "Name": "Podcasts", + "locale": "en", + "link": "/podcasts", + "local": true + } + ] + }, + { + "Description": "Manage podcasts, series and episodes", + "Name": "Administration", + "admin": true, + "link": "/admin", + "menu_items": [ + { + "Name": "Andmin Login", + "locale": "en", + "loggedin": false, + "link": "/admin/login", + "local": true + }, + { + "Name": "Change Password", + "loggedin": true, + "locale": "en", + "link": "/admin/setpassword", + "local": true + }, + { + "Name": "Invite User", + "locale": "en", + "loggedin": true, + "link": "/admin/invitation", + "local": true + }, + { + "Name": "Logout", + "locale": "en", + "loggedin": true, + "link": "#logout", + "local": true + } + ] + } + ] +} \ No newline at end of file diff --git a/cypress/fixtures/pod-cover0.jpg b/cypress/fixtures/pod-cover0.jpg new file mode 100644 index 0000000..78063ed Binary files /dev/null and b/cypress/fixtures/pod-cover0.jpg differ diff --git a/cypress/fixtures/pod-cover1.jpg b/cypress/fixtures/pod-cover1.jpg new file mode 100644 index 0000000..1686c7e Binary files /dev/null and b/cypress/fixtures/pod-cover1.jpg differ diff --git a/cypress/fixtures/pod-cover2.jpg b/cypress/fixtures/pod-cover2.jpg new file mode 100644 index 0000000..9418bda Binary files /dev/null and b/cypress/fixtures/pod-cover2.jpg differ diff --git a/cypress/fixtures/pod-cover3.jpg b/cypress/fixtures/pod-cover3.jpg new file mode 100644 index 0000000..122817f Binary files /dev/null and b/cypress/fixtures/pod-cover3.jpg differ diff --git a/cypress/fixtures/podcasts.json b/cypress/fixtures/podcasts.json new file mode 100644 index 0000000..a01d0b0 --- /dev/null +++ b/cypress/fixtures/podcasts.json @@ -0,0 +1,60 @@ +[ + { + "id": 1, + "cover_file": "/s/covers/asdfasdf/podcast-philipper.jpg", + "title": "asdfasdf", + "slug": "asdfasdf", + "subtitle": "asdfasdfsdaf", + "author": "asdsadf", + "summary": "", + "description": "", + "language_id": 1, + "category_id": 1, + "type_id": 1, + "explicit": false, + "link": "", + "copyright": "", + "owner_name": "adadsf", + "owner_email": "a@b.de", + "lastbuild": "", + "state": -1, + "external_id": -1, + "draft": false, + "apple_url": "", + "spotify_url": "", + "google_url": "", + "stitcher_url": "", + "createdAt": "2024-02-02T14:57:35.000Z", + "updatedAt": "2024-02-02T14:57:35.000Z", + "deletedAt": null + }, + { + "id": 2, + "cover_file": "/s/covers/das_ist_der_titel/pod-cover.jpg", + "title": "Das ist der Titel", + "slug": "das_ist_der_titel", + "subtitle": "Untertitel", + "author": "Das Autor", + "summary": "", + "description": "", + "language_id": 1, + "category_id": 1, + "type_id": 1, + "explicit": false, + "link": "", + "copyright": "", + "owner_name": "Person", + "owner_email": "person@hier.de", + "lastbuild": "", + "state": -1, + "external_id": -1, + "draft": false, + "apple_url": "", + "spotify_url": "", + "google_url": "", + "stitcher_url": "", + "createdAt": "2024-02-02T18:29:35.000Z", + "updatedAt": "2024-02-02T18:29:35.000Z", + "deletedAt": null + } +] \ No newline at end of file diff --git a/cypress/fixtures/serie-cover3.jpg b/cypress/fixtures/serie-cover3.jpg new file mode 100644 index 0000000..122817f Binary files /dev/null and b/cypress/fixtures/serie-cover3.jpg differ diff --git a/cypress/fixtures/serie-cover4.jpg b/cypress/fixtures/serie-cover4.jpg new file mode 100644 index 0000000..24ce467 Binary files /dev/null and b/cypress/fixtures/serie-cover4.jpg differ diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..66ea16e --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 0000000..0e7290a --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9086842 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.1' + +services: + podcashde-new: + build: + context: ./podkashde + dockerfile: Dockerfile + container_name: podcashde-new + env_file: .env.docker + ports: + - 8333:80 + volumes: + - ./public:/var/www + - ./data:/data \ No newline at end of file diff --git a/docker/docker-prod-api.yml b/docker/docker-prod-api.yml new file mode 100644 index 0000000..0ec7f71 --- /dev/null +++ b/docker/docker-prod-api.yml @@ -0,0 +1,13 @@ +version: '3.1' + +services: + podkashde.app: + image: ghcr.io/ccfreiburg/podkashde:latest + labels: + - "com.centurylinklabs.watchtower.enable=true" + container_name: podkashde.app + ports: + - 3333:3000 + env_file: .env + volumes: + - ../public/s:/var/www/public/s diff --git a/docker/nginx/default b/docker/nginx/default index fa81755..6fe36ef 100644 --- a/docker/nginx/default +++ b/docker/nginx/default @@ -18,15 +18,25 @@ server { tcp_nodelay on; keepalive_timeout 65; access_log /var/log/nginx/mp3access.log custom buffer=1k flush=1m; - root /var/www/public; + root /var/www/; } location /s/ { # all other static file - root /var/www/public; + root /var/www/; + } + + location /img/ { + # all other static file + root /var/www/; + } + + location /json/ { + # all other static file + root /var/www/; } - location /archiv { + location /archiv/ { # backup archive autoindex on; root /var/www; @@ -38,11 +48,18 @@ server { proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://localhost:3000; + proxy_pass http://localhost:3003; access_log /var/log/nginx/mp3access.log custom buffer=1k flush=1m; # access_log /var/log/nginx/mediaaccess.log; } + location /api/ { + proxy_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://localhost:3003; + } + location / { proxy_buffering off; proxy_set_header Host $host; diff --git a/docker/nginx/startup-dev.sh b/docker/nginx/startup-dev.sh old mode 100755 new mode 100644 diff --git a/docker/nginx/startup.sh b/docker/nginx/startup.sh index 006f637..e97e8ea 100755 --- a/docker/nginx/startup.sh +++ b/docker/nginx/startup.sh @@ -1,3 +1,31 @@ #!/usr/bin/dumb-init /bin/sh /usr/sbin/nginx & -node /var/www/server/index.mjs \ No newline at end of file + +clean_up() +{ + echo "Exiting..." + kill $bePID + exit +} +# Trigger cleanup on CTRL + C +trap clean_up INT EXIT + +env + +cd /app +cd backend +yarn start & +bePID=$! + +CHECKBE="" +i=0 +while [ -z "${CHECKBE}" -a $i -lt 20 ]; +do + CHECKBE=`curl -s http://localhost:3003/api/podcasts -I | grep 200` + sleep 2s + i=$((i+1)) +done + +cd .. +yarn build +node .output/server/index.mjs \ No newline at end of file diff --git a/error.vue b/error.vue deleted file mode 100644 index e271c81..0000000 --- a/error.vue +++ /dev/null @@ -1,13 +0,0 @@ - \ No newline at end of file diff --git a/example.settings.json b/example.settings.json deleted file mode 100644 index 374c9fb..0000000 --- a/example.settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - JWT_ACCESS_TOKEN_SECRET: "a-secretaasdfadfasdgasdgasd", - JWT_REFRESH_TOKEN_SECRET: "aSAFDSDFDSFsdfadfasdgasdgasd", - JWT_URL_TOKEN_SECRET: "lkghfladskhgakhdgkasd", - ADMIN_USER: "admin", - ADMIN_PASSWORD: "password" -} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index 17943a4..d9fcdeb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -32,6 +32,7 @@ "sessionexpired": "Anmeldung veraltet. Bitte neu anmelden...", "loginfailed": "Passwort oder Nutzername falsch", "invitelink": "Einladungslink erstellen", + "invitelinktext": "Einladungslink", "passwordset": "Das neue Passwort wurde erfolgreich gesetzt", "invitationtitle": "Nutzer einladen", "validation": { @@ -46,6 +47,7 @@ "edit": "Podcast bearbeiten", "change": "Podcast zuordnen", "addImage": "Klicken um Bild hinzuzufügen", + "notfound": "Podcast wurde nicht gefunden", "inthis": "in diesem Podcast", "label": { "img": "Klicken um Bild hinzuzufügen", @@ -96,8 +98,9 @@ "episode": "Folge", "recent": "Kürzlich erschienen", "new": "Neue Folge", - "add": "Folge hinzuzufügen", + "add": "Folge hinzufügen", "edit": "Folge bearbeiten", + "notfound": "Folge wurde nicht gefunden", "forpodcast": "für", "edit_short": "bearbeiten", "explicit": "explizit", @@ -151,6 +154,7 @@ "episodes": "Folgen der Serie", "edit": "Serie bearbeiten", "inthis": "in dieser Serie", + "notfound": "Serie wurde nicht gefunden", "tothepisodes": "zu den Folgen...", "label": { "img": "klicken um Bild hinzuzufügen", diff --git a/locales/en.json b/locales/en.json index 93a22d8..0e47368 100644 --- a/locales/en.json +++ b/locales/en.json @@ -32,6 +32,7 @@ "sessionexpired": "Session has expired. Please login ...", "loginfailed": "Wrong password or user does not exist", "invitelink": "Get invitation link", + "invitelinktext": "Invitation link", "invitationtitle": "Invite user", "passwordset": "The new password has been set successfully", "validation": { @@ -46,6 +47,7 @@ "edit": "Edit Podcast", "change": "Assign all Episodes to another Podcast", "addImage": "click to add image", + "notfound": "Podcast not found", "inthis": "in this podcast", "label": { "img": "click to add image", @@ -99,6 +101,7 @@ "add": "Add Episode", "edit": "Edit Episode", "forpodcast": "for", + "notfound": "Episode not found", "edit_short": "edit", "explicit": "explicit", "blocked": "blocked", @@ -147,6 +150,7 @@ "edit": "Edit Series", "episodes": "Series Episodes", "inthis": "in this series", + "notfound": "Series not found", "tothepisodes": "To the episodes...", "label": { "img": "klicken um Bild hinzuzufügen", diff --git a/middleware/authentication.ts b/middleware/authentication.ts deleted file mode 100644 index abfc76b..0000000 --- a/middleware/authentication.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default defineNuxtRouteMiddleware(async(to, from) => { - try { - var user = useAuth().useAuthToken() - if (!user || !user.value) { - return navigateTo('/admin/login') - } - } catch { - return navigateTo('/admin/login') - } -}) \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index 7d938ec..065fdc4 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,24 +1,32 @@ -import fs from "fs-extra" -import path from "path" -import type { Nitro } from 'nitropack'; -import { defineNuxtConfig } from "nuxt/config"; +// https://nuxt.com/docs/api/configuration/nuxt-config + +function setEnv( direct: string | undefined, defaultval = "", indirect: string | undefined = undefined, indirect_postfix: string = "") : string { + if (direct && direct.length > 0) + return direct + if (indirect && indirect.length > 0) + return indirect + indirect_postfix + return defaultval +} + +function setEnvBool( envvar: string | undefined, defaultval: boolean | undefined = undefined ) : boolean { + if (envvar && envvar.length>0 && envvar.toLowerCase()!=="false") + return true + else + return defaultval ?? false +} + +function setEnvUndefinedWhenEmpty( envvar: string | undefined ) :string | undefined { + if (envvar && envvar.length>0 && envvar.toLowerCase()!=="false") + return envvar + else + return undefined +} -// https://v3.nuxtjs.org/api/configuration/nuxt.config export default defineNuxtConfig({ - modules: ['@nuxtjs/tailwindcss', '@nuxtjs/color-mode', '@nuxtjs/i18n','nuxt-umami'], - ssr: true, - target: 'server', - head: { - title: "Podcasts", - meta: { - charset: "UTF-8", - } - }, - generate: { - exclude: [ - /^\/admin/ // path starts with /admin - ] - }, + modules: [ + '@nuxtjs/i18n', '@nuxtjs/tailwindcss', '@nuxtjs/color-mode' + ], + extends: [ 'nuxt-umami' ], i18n: { strategy: 'prefix_except_default', defaultLocale: 'de', @@ -36,89 +44,44 @@ export default defineNuxtConfig({ ], lazy: true, langDir: 'locales', - vueI18n: { - fallbackLocale: 'de' - } - }, - umami: { - enable: true, // enable the module? true by default - autoTrack: true, - doNotTrack: false, - cache: false, - domains: process.env.UMAMI_DOMAINS || 'podcast.ccfreiburg.de', - websiteId: process.env.UMAMI_KEY || '4b79e0da-e70b-430b-b0ea-978691c32f55', - scriptUrl: process.env.UMAMI_AP || 'https://analytics.ccfreiburg.de/umami.js', - }, - tailwindcss: { - cssPath: '~/assets/css/tailwind.css', - configPath: 'tailwind.config.js', - exposeConfig: false, - injectPosition: 0, - viewer: true, }, colorMode: { - darkMode: 'class', - preference: 'light', - fallback: 'light', + preference: 'system', // default value of $colorMode.preference + fallback: 'light', // fallback value if not system preference found classSuffix: '', classPrefix: '', }, - postcss: { - plugins: { - tailwindcss: {}, - autoprefixer: { - grid: true, - flexbox: true - }, - }, + // css: [ './assets/css/tailwind.css' + // ], + // app: { + // pageTransition: { + // name: 'page', + // mode: 'out-in' + // } + // }, + runtimeConfig: { + public: { + url: process.env.NUXT_PUBLIC_URL, + appBase: setEnv(process.env.NUXT_PUBLIC_APP_BASE, 'http://localhost:3000', process.env.NUXT_PUBLIC_URL), + apiBase: setEnv(process.env.NUXT_PUBLIC_API_BASE, 'http://localhost:3003/api/', process.env.NUXT_PUBLIC_URL, "/api/") , + mediaBase: setEnv(process.env.NUXT_PUBLIC_MEDIA_BASE, 'http://localhost:3003', process.env.NUXT_PUBLIC_URL), + skin: setEnv(process.env.NUXT_PUBLIC_SKIN,''), + logo: setEnv(process.env.NUXT_PUBLIC_LOGO, '/img/logo.png'), + logoDark: setEnv(process.env.NUXT_PUBLIC_LOGO_DARK, '/img/logo-w.png'), + enableDarkMode: setEnvUndefinedWhenEmpty(process.env.NUXT_PUBLIC_ENABLE_DARK_MODE), + umamiActive: setEnvBool(process.env.NUXT_PUBLIC_UMAMI_ID) + } }, - nitro: { - preset: 'node-server', - hooks: { - compiled(nitro: Nitro) { - const packages = [] - packages.push({ - dest:path.join( - nitro.options.output.dir, - 'server', - 'node_modules', - 'parse5'), - src: path.join( - '.', - 'node_modules', - 'parse5', - 'dist', - 'cjs') - }) - packages.push({ - dest:path.join( - nitro.options.output.dir, - 'server', - 'node_modules', - 'entities'), - src: path.join( - '.', - 'node_modules', - 'entities') - }) - packages.push({ - dest:path.join( - nitro.options.output.dir, - 'server', - 'node_modules', - 'sqlite3'), - src: path.join( - '.', - 'node_modules', - 'sqlite', - 'lib') - }) - try { - packages.forEach(pack => { - fs.copySync(pack.src, pack.dest, {overwrite: true}) - }); - } catch (err) {} - }, - }, - } + appConfig: { + umami: { + autoTrack: setEnvBool(process.env.NUXT_PUBLIC_UMAMI_ID), + version: 2 + }}, + // nitro: { + // routeRules: { + // "/api/**": { proxy: 'localhost:3003' }, + // "/s/**": { proxy: 'localhost:3003' } + // } + // }, + devtools: { enabled: false } }) diff --git a/package.json b/package.json index 499a346..1c382b4 100644 --- a/package.json +++ b/package.json @@ -1,58 +1,35 @@ { - "private": true, - "scripts": { - "build": "nuxt build", - "dev": "nuxt dev", - "generate": "nuxt generate", - "preview": "nuxt preview", - "test:unit": "vitest run --dir ./tests/unit", - "test:comp": "vitest run --config ./tests/components/vitest.config.js --dir ./tests/components", - "test:e2e": "vitest run --dir ./tests/e2e", - "test:int": "vitest run --dir ./tests/integration", - "test": "yarn test:unit && yarn test:comp && yarn test:e2e", - "watch:unit": "vitest --dir ./tests/unit", - "watch:comp": "vitest --config ./tests/components/vitest.config.js --dir ./tests/components", - "start": "ts-node src/index.ts", - "typeorm": "typeorm-ts-node-commonjs", - "migration": "typeorm-ts-node-commonjs -d ./server/db/datasource.ts migration:generate ./server/db/migrations/auth" - }, - "devDependencies": { - "@nuxt/postcss8": "^1.1.3", - "@nuxt/test-utils-edge": "^3.0.0-rc.8-27698890.b90d286", - "@nuxtjs/color-mode": "^3.1.4", - "@nuxtjs/i18n": "^8.0.0-beta.7", - "@nuxtjs/tailwindcss": "^6.1.3", - "@types/busboy": "^1.5.0", - "@types/node": "^16.11.10", - "@types/xml2js": "^0.4.11", - "@vitejs/plugin-vue": "^3.0.3", - "autoprefixer": "^10.4.8", - "fs-extra": "^10.1.0", - "jsdom": "^20.0.0", - "nuxt-umami": "^1.2.0", - "postcss": "^8.4.14", - "sqlite3": "^5.1.4", - "tailwindcss": "^3.1.6", - "ts-node": "10.7.0", - "typescript": "4.5.2" - }, - "dependencies": { - "@testing-library/vue": "^6.6.1", - "@types/jsonwebtoken": "^8.5.9", - "@types/node": "^16.11.10", - "@vueuse/core": "^8.9.4", - "bcrypt": "^5.0.1", - "busboy": "^1.6.0", - "id3-parser": "^2.0.0", - "jsonwebtoken": "^8.5.1", - "jwt-decode": "^3.1.2", - "node-id3": "^0.2.4", - "nuxt": "^3.3.1", - "podcast": "^2.0.1", - "reflect-metadata": "^0.1.13", - "sqlite3": "^5.1.4", - "typeorm": "^0.3.7", - "url-pattern": "^1.0.3", - "vitest": "^0.22.1" - } + "name": "nuxt-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare", + "dev:be": "cd backend; yarn dev", + "dev:cy": "npx cypress open --env NUXT_MODE=development", + "run:cy": "npx cypress run --env NUXT_MODE=development", + "rec:cy": "npx cypress run --record --key 6b81c56a-4e73-4cfd-a36d-acab9f138d57 --env NUXT_MODE=development", + "clean:cy": "rm -r ./backend/public/s && rm ./backend/data/podcasts.sqlite" + }, + "devDependencies": { + "@nuxtjs/color-mode": "^3.4.2", + "@nuxtjs/i18n": "^8.3.3", + "@nuxtjs/tailwindcss": "^6.12.1", + "cypress": "13.6.6", + "nuxt": "^3.12.4", + "typescript": "^5.3.3", + "vue": "^3.4.36", + "vue-router": "^4.4.3" + }, + "dependencies": { + "id3-parser": "^3.0.0", + "jwt-payloader": "^1.0.1", + "nuxt-umami": "^2.6.3" + }, + "resolutions": { + "string-width": "4.2.3" + } } diff --git a/pages/[episodeslug]/index.vue b/pages/[episodeslug]/index.vue index 934bb82..74ba035 100644 --- a/pages/[episodeslug]/index.vue +++ b/pages/[episodeslug]/index.vue @@ -1,16 +1,15 @@ diff --git a/pages/admin/[episodeslug]/index.vue b/pages/admin/[episodeslug]/index.vue index c944257..33dd13e 100644 --- a/pages/admin/[episodeslug]/index.vue +++ b/pages/admin/[episodeslug]/index.vue @@ -1,37 +1,23 @@ \ No newline at end of file diff --git a/pages/admin/import.vue b/pages/admin/import.vue index ad1469d..ecbef42 100644 --- a/pages/admin/import.vue +++ b/pages/admin/import.vue @@ -1,17 +1,17 @@ diff --git a/pages/admin/invitation.vue b/pages/admin/invitation.vue index 4a651fc..e638b33 100644 --- a/pages/admin/invitation.vue +++ b/pages/admin/invitation.vue @@ -1,19 +1,16 @@ diff --git a/pages/admin/login.vue b/pages/admin/login.vue index c8c045d..4623ee4 100644 --- a/pages/admin/login.vue +++ b/pages/admin/login.vue @@ -1,68 +1,62 @@ diff --git a/pages/admin/new-podcast.vue b/pages/admin/new-podcast.vue index 990015d..1b115a9 100644 --- a/pages/admin/new-podcast.vue +++ b/pages/admin/new-podcast.vue @@ -1,44 +1,29 @@ \ No newline at end of file diff --git a/pages/admin/new-serie.vue b/pages/admin/new-serie.vue index 5960086..aaeaa74 100644 --- a/pages/admin/new-serie.vue +++ b/pages/admin/new-serie.vue @@ -1,38 +1,30 @@ + + - \ No newline at end of file diff --git a/pages/admin/podcast/[slug]/index.vue b/pages/admin/podcast/[slug]/index.vue index d0ddbc4..ae2c286 100644 --- a/pages/admin/podcast/[slug]/index.vue +++ b/pages/admin/podcast/[slug]/index.vue @@ -1,51 +1,44 @@ +async function ondelete() { + await remove(); + router.push("/"); +} + diff --git a/pages/admin/podcast/[slug]/new-episode.vue b/pages/admin/podcast/[slug]/new-episode.vue index 300369a..193133c 100644 --- a/pages/admin/podcast/[slug]/new-episode.vue +++ b/pages/admin/podcast/[slug]/new-episode.vue @@ -1,41 +1,32 @@ \ No newline at end of file diff --git a/pages/admin/serie/[slug]/index.vue b/pages/admin/serie/[slug]/index.vue index 0800feb..558ca86 100644 --- a/pages/admin/serie/[slug]/index.vue +++ b/pages/admin/serie/[slug]/index.vue @@ -1,35 +1,28 @@ - diff --git a/pages/admin/setpassword.vue b/pages/admin/setpassword.vue index 7257cbe..e496af9 100644 --- a/pages/admin/setpassword.vue +++ b/pages/admin/setpassword.vue @@ -1,13 +1,9 @@ \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index 2107109..b27919e 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,10 +1,23 @@ \ No newline at end of file +const { refresh, episodes } = useEpisodes(); +onBeforeMount(() => { + const route = useRoute() + refresh(); +}) +onMounted(() =>{ + const router = useRouter() + router.replace({ + ...router.currentRoute, + query: { + } + })}) + diff --git a/pages/podcast/[slug]/index.vue b/pages/podcast/[slug]/index.vue index dd146c0..19ffa4f 100644 --- a/pages/podcast/[slug]/index.vue +++ b/pages/podcast/[slug]/index.vue @@ -1,80 +1,91 @@ + \ No newline at end of file diff --git a/pages/podcasts.vue b/pages/podcasts.vue index 309c0f1..941b846 100644 --- a/pages/podcasts.vue +++ b/pages/podcasts.vue @@ -1,41 +1,35 @@ diff --git a/pages/serie/[slug]/index.vue b/pages/serie/[slug]/index.vue index 19296df..a069a50 100644 --- a/pages/serie/[slug]/index.vue +++ b/pages/serie/[slug]/index.vue @@ -1,63 +1,78 @@ diff --git a/pages/series.vue b/pages/series.vue new file mode 100644 index 0000000..51c7b45 --- /dev/null +++ b/pages/series.vue @@ -0,0 +1,84 @@ + + + diff --git a/public/app.settings.json b/public/app.settings.json deleted file mode 100644 index 1f791b7..0000000 --- a/public/app.settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "baseUrl": "http://localhost:3000", - "defaultRoute": "/recent", - "menuSourdce": "/api/menu", - "skin": "theme-ccf", - "logo": "/img/ccf-logo.png", - "logo_w": "/img/ccf-logo-w.png", - "enableDarkOption": true -} \ No newline at end of file diff --git a/public/menu-ex.json b/public/menu-ex.json deleted file mode 100644 index 4401134..0000000 --- a/public/menu-ex.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "defaultBase": "https://ccf.calvarychapel.de", - "en": [ - { - "name": "Come in", - "description": "Plan your visit to Calvary Chapel Freiburg", - "entries": [ - { - "name": "Ministries", - "order": 10, - "slug": "ministries" - } - ] - } - ], - "de": [ - { - "name": "Komm vorbei", - "description": "Plane deinen Besuch in der Calvary Chapel Freiburg", - "entries": [ - { - "name": "Heimathafen", - "order": 10, - "slug": "ministries" - } - ] - }, - { - "name": "Hör rein", - "description": "Höre unsere Predigen, Inputs und Biblestudies oder lies Neues und Andachten", - "entries": [ - { - "name": "Aktuelle Predigten", - "order": 10, - "slug": "predigten" - } - ] - }, - { - "name": "Mach mit", - "description": "Erforsche die Möglichkeiten dich einzubringen", - "entries": [ - { - "name": "Mitgliedschaft", - "order": 10, - "slug": "mitgliedschaft" - } - ] - }, - { - "name": "Sei dabei", - "description": "Erfahre was Mitgliedschaft bedeutet und an welchen zu welchen Gruppen du kommen kannt", - "entries": [ - { - "name": "Sommerlager", - "order": 10, - "slug": "sloa" - } - ] - }, - { - "name": "Über uns", - "description": "Erfahre mehr über uns und unseren Glauben", - "entries": [ - { - "name": "Calvary was?", - "order": 10, - "slug": "calvarychapel-was" - } - ] - } - ] -} diff --git a/public/menu.json b/public/menu.json index 514122b..a220e7c 100644 --- a/public/menu.json +++ b/public/menu.json @@ -2,242 +2,207 @@ "defaultBase": "https://ccfreiburg.de", "en": [ { - "name": "About us", - "slug": "about-us/", - "description": "Get to know us", + "name": "Visit us", + "description": "Plan your visit to Calvary Chapel Freiburg", "entries": [ { - "name": "Our Leadership", + "name": "Services", "order": 10, - "slug": "about-us/our-leadership/" + "slug": "worship-services" }, { - "name": "Our Name", + "name": "Locations", "order": 20, - "slug": "about-us/our-name/" + "slug": "buildings" }, { - "name": "Our Faith", + "name": "Heimathafen", "order": 30, - "slug": "about-us/our-faith/" + "slug": "en-heimathafen" }, { - "name": "Our Core Values", + "name": "Events", "order": 40, - "slug": "about-us/our-faith/core-values/" + "slug": "special-events" } ] }, { "name": "Connect", - "slug": "connect/", - "description": "Connect with the Church", + "description": "Connect with us and become part of Calvary Chapel Freiburg", "entries": [ - { - "name": "Membership", - "order": 10, - "slug": "connect/membership/" - }, { "name": "Small Groups", "order": 10, - "slug": "connect/smallgroups/" + "slug": "article/kleingruppen" }, { "name": "Encouraging People", - "order": 10, - "slug": "connect/encouraging-people/" + "order": 20, + "slug": "encouraging-people" }, { - "name": "Heimathafen", - "order": 10, - "slug": "connect/heimathafen/" - }, - { - "name": "Royal Rangers 393 (de)", - "order": 10, + "name": "Royal Rangers 393 (DE)", + "order": 30, "slug": "https://rr393.de" }, { - "name": "Echtzeit (de)", - "order": 10, - "slug": "connecten/echtzeit/" - }, + "name": "EchtZeit", + "order": 40, + "slug": "article/echtzeit" + }, { - "name": "Junge Erwachsene (de)", - "order": 10, - "slug": "junge-erwachsene/" - }, + "name": "Young Adults", + "order": 40, + "slug": "young-adults" + }, { "name": "Mens Ministry", - "order": 10, - "slug": "connect/mens-ministry/" + "order": 40, + "slug": "mens-ministry" }, { - "name": "Sommerlager (de)", - "order": 10, - "slug": "connecten/sola/" + "name": "Summer Camp (DE)", + "order": 40, + "slug": "https://ccfreiburg.de/sola/" } ] }, { "name": "Hör rein", - "slug": "https://podcast.ccfreiburg.de", "description": "Höre unsere Predigen, Inputs und Biblestudies oder lies Neues und Andachten", "entries": [ { "name": "Recent Sermons", "order": 10, - "slug": "recent", - "local": true + "slug": "https://podcast.ccfreiburg.de/en/recent" }, { "name": "Current Series", "order": 20, - "slug": "serie", - "local": true + "slug": "https://podcast.ccfreiburg.de/en/serie" }, { "name": "Podcasts", "order": 10, - "slug": "podcasts", - "local": true + "slug": "https://podcast.ccfreiburg.de/en/podcasts" }, { "name": "Podcast Admin", "order": 10, - "slug": "admin", - "local": true + "slug": "https://podcast.ccfreiburg.de/en/admin" } ] }, { - "name": "Jouarnal", - "description": "Discover Content", - "slug": "journal-en/", + "name": "About us", + "description": "Get to know us", "entries": [ { - "name": "Overview", + "name": "Our Leadership", "order": 10, - "slug": "journal-en/" - } - ] - }, - { - "name": "Give", - "slug": "give", - "description": "Find out how to support us", - "entries": [ + "slug": "our-leadership/" + }, { - "name": "Overview", - "order": 10, - "slug": "give" + "name": "Our Core Values", + "order": 20, + "slug": "our-core-values" + }, + { + "name": "Membership", + "order": 20, + "slug": "membership" + }, + { + "name": "Give", + "order": 30, + "slug": "giving" + }, + { + "name": "Contact", + "order": 40, + "slug": "contact" } ] } - ], "de": [ { - "name": "Über uns", - "slug": "ueber-uns/", - "description": "Lerne uns kennen", + "name": "Komm vorbei", + "description": "Plane deinen Besuch in der Calvary Chapel Freiburg", "entries": [ { - "name": "Unsere Leitung", + "name": "Gottesdienste", "order": 10, - "slug": "ueber-uns/unsere-leitung/" - }, - { - "name": "Unser Name", - "order": 20, - "slug": "ueber-uns/unser-name/" - }, - { - "name": "Unser Glaube", - "order": 30, - "slug": "ueber-uns/unser-glaube/" + "slug": "gottesdienste" }, { - "name": "Unsere Grundwerte", - "order": 40, - "slug": "ueber-uns/unser-glaube/grundwerte/" + "name": "Gebäude", + "order": 10, + "slug": "gebaeude" }, { - "name": "Info & Impressum", - "order": 50, - "slug": "kontakt" + "name": "Heimathafen", + "order": 10, + "slug": "heimathafen" }, { - "name": "Datenschutzerklärung", - "order": 60, - "slug": "datenschutzerklarung" + "name": "Events", + "order": 10, + "slug": "events" } - ] }, { "name": "Connect", - "slug": "connecten/", - "description": "Connect with the Church", + "description": "Tritt mit uns in Verbindung und werde teil der Calvary Chapel Freiburg", "entries": [ - { - "name": "Mitgliedschaft", - "order": 10, - "slug": "connecten/mitgliedschaft/" - }, { "name": "Kleingruppen", "order": 10, - "slug": "connecten/kleingruppen/" - }, + "slug": "article/kleingruppen" + }, { "name": "Menschen fördern", "order": 10, - "slug": "connecten/menschen-foerdern/" - }, - { - "name": "Heimathafen", - "order": 10, - "slug": "connecten/heimathafen/" - }, + "slug": "menschen-foerdern" + }, { "name": "Royal Rangers 393", "order": 10, "slug": "https://rr393.de" - }, + }, { "name": "Echtzeit", "order": 10, - "slug": "connecten/echtzeit/" - }, + "slug": "article/echtzeit/" + }, { "name": "Junge Erwachsene", "order": 10, - "slug": "junge-erwachsene/" - }, + "slug": "junge-erwachsene" + }, { "name": "Männerarbeit", "order": 10, - "slug": "connecten/maennerarbeit/" - }, + "slug": "maennerarbeit" + }, { "name": "Sommerlager", "order": 10, - "slug": "connecten/sola/" + "slug": "sola" } ] - }, + }, { "name": "Hör rein", - "slug": "https://podcast.ccfreiburg.de", "description": "Höre unsere Predigen, Inputs und Biblestudies oder lies Neues und Andachten", "entries": [ { "name": "Aktuelle Predigten", "order": 10, - "slug": "recent", - "local": true + "slug": "https://podcast.ccfreiburg.de/recent", + "local": false }, { "name": "Aktuelle Serien", @@ -260,28 +225,40 @@ ] }, { - "name": "Jouarnal", - "slug": "journal/", - "description": "Content enddecken", + "name": "Erfahre mehr", + "description": "Weitere Informationen zur Calvary Chapel Feiburg, unsere Geschichte, Werte ...", "entries": [ { - "name": "Übersichtsseite", + "name": "Unsere Leitung", "order": 10, - "slug": "journal/" - } - ] - }, - { - "name": "Geben", - "slug": "geben/", - "description": "Wie du mit Spenden unterstützen kannst", - "entries": [ + "slug": "unsere-leitung" + }, { - "name": "Übersichtsseite", - "order": 10, + "name": "Calvary was?", + "order": 20, + "slug": "calvary-was" + }, + { + "name": "Unsere Grundwerte", + "order": 40, + "slug": "unsere-grundwerte" + }, + { + "name": "Geben", + "order": 50, "slug": "geben" + }, + { + "name": "Kontakt", + "order": 60, + "slug": "kontakt" + }, + { + "name": "Mitgliedschaft", + "order": 70, + "slug": "mitgliedschaft" } ] } ] -} +} \ No newline at end of file diff --git a/rectest.sh b/rectest.sh new file mode 100755 index 0000000..c2372cc --- /dev/null +++ b/rectest.sh @@ -0,0 +1,40 @@ +#!/usr/bin/bash +function clean_up() +{ + echo "Exiting..." + kill $fePID + kill $bePID + exit +} +# Trigger cleanup on CTRL + C +trap clean_up SIGINT EXIT + +cd backend +yarn +node ./node_modules/.bin/ts-node src/index.ts > /dev/null & +bePID=$! +CHECKBE="" +i=0 +while [ -z "${CHECKBE}" -a $i -lt 20 ]; +do + CHECKBE=`curl -s http://localhost:3003/api/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done +cd .. +yarn +yarn build +node .output/server/index.mjs > /dev/null & +fePID=$! +CHECKFE="" +i=0 +while [ -z "${CHECKFE}" -a $i -lt 20 ]; +do + CHECKFE=`curl -s http://localhost:3000/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done + +trap clean_up ERR + +npx cypress run --record --key 6b81c56a-4e73-4cfd-a36d-acab9f138d57 diff --git a/rundev.sh b/rundev.sh new file mode 100644 index 0000000..67bc978 --- /dev/null +++ b/rundev.sh @@ -0,0 +1,39 @@ +#!/usr/bin/dumb-init /bin/sh +/usr/sbin/nginx & + +function clean_up() +{ + echo "Exiting..." + kill $fePID + kill $bePID + exit +} +# Trigger cleanup on CTRL + C +trap clean_up SIGINT EXIT + +cd backend +yarn +yarn dev & +CHECKBE="" +i=0 +while [ -z "${CHECKBE}" -a $i -lt 20 ]; +do + CHECKBE=`curl -s http://localhost:3003/api/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done + +cd .. +yarn +yarn dev & +fePID=$! +CHECKFE="" +i=0 +while [ -z "${CHECKFE}" -a $i -lt 20 ]; +do + CHECKFE=`curl -s http://localhost:3000/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done + +trap clean_up ERR \ No newline at end of file diff --git a/runtest.sh b/runtest.sh new file mode 100755 index 0000000..a916c4e --- /dev/null +++ b/runtest.sh @@ -0,0 +1,40 @@ +#!/usr/bin/bash +function clean_up() +{ + echo "Exiting..." + kill $fePID + kill $bePID + exit +} +# Trigger cleanup on CTRL + C +trap clean_up SIGINT EXIT + +cd backend +yarn +node ./node_modules/.bin/ts-node src/index.ts > ../cypress/logs/back-end.log & +bePID=$! +CHECKBE="" +i=0 +while [ -z "${CHECKBE}" -a $i -lt 20 ]; +do + CHECKBE=`curl -s http://localhost:3003/api/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done +cd .. +yarn +yarn build +node .output/server/index.mjs > ./cypress/logs/front-end.log & +fePID=$! +CHECKFE="" +i=0 +while [ -z "${CHECKFE}" -a $i -lt 20 ]; +do + CHECKFE=`curl -s http://localhost:3000/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done + +trap clean_up ERR + +npx cypress run diff --git a/server/api/auth/checktoken.ts b/server/api/auth/checktoken.ts deleted file mode 100644 index 98ca628..0000000 --- a/server/api/auth/checktoken.ts +++ /dev/null @@ -1,32 +0,0 @@ -import bcrypt from "bcrypt" -import { decodeUrlToken, generateAccessToken, generateRefreshToken, sendRefreshToken } from "../../jwt.js" -import { sendError } from "h3" -import { getUserByEmail, getUserByUserName, sanitizeUserForFrontend } from "~~/server/services/userService.js" -import { createSession } from "~~/server/services/sessionService.js" - -export default defineEventHandler(async (event) => { - const query = await getQuery(event) - const token: string = query.token - - if (!token) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Ivalid params' - })) - } - - const data = decodeUrlToken(token) - const user = await getUserByUserName(data?.username) - - if (!user) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Username or password is invalid' - })) - } - - return { - access: data.purpose, user: sanitizeUserForFrontend(user) - } - -}) \ No newline at end of file diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts deleted file mode 100644 index 798cae3..0000000 --- a/server/api/auth/login.post.ts +++ /dev/null @@ -1,48 +0,0 @@ -import bcrypt from "bcrypt" -import { generateAccessToken, generateRefreshToken, sendRefreshToken } from "../../jwt.js" -import { sendError } from "h3" -import { getUserByEmail, getUserByUserName, sanitizeUserForFrontend } from "~~/server/services/userService.js" -import { createSession } from "~~/server/services/sessionService.js" - -export default defineEventHandler(async (event) => { - const body = await readBody(event) - const username: string = body.username - const password: string = body.password - - if (!username || !password) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Ivalid params' - })) - } - - const user = await getUserByUserName(username) - - if (!user) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Username or password is invalid' - })) - } - - const doesThePasswordMatch = await bcrypt.compare(password, user.password) - - if (!doesThePasswordMatch) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Username or password is invalid' - })) - } - - const accessToken = generateAccessToken(user) - const refreshToken = generateRefreshToken(user) - - await createSession( refreshToken, user.id ) - - sendRefreshToken(event, refreshToken) - - return { - access_token: "accessToken", user: sanitizeUserForFrontend(user) - } - -}) \ No newline at end of file diff --git a/server/api/auth/logout.post.ts b/server/api/auth/logout.post.ts deleted file mode 100644 index 413aeee..0000000 --- a/server/api/auth/logout.post.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { sendRefreshToken } from "~~/server/jwt" -import { removeSession } from "~~/server/services/sessionService" -import { getCookie} from 'h3' - -export default defineEventHandler(async (event) => { - try { - const refreshToken = getCookie(event, "refresh_token") as string - await removeSession(refreshToken) - } catch (error) { } - - sendRefreshToken(event, null) - - return { message: 'Done' } -}) \ No newline at end of file diff --git a/server/api/auth/password.post.ts b/server/api/auth/password.post.ts deleted file mode 100644 index fa620c7..0000000 --- a/server/api/auth/password.post.ts +++ /dev/null @@ -1,59 +0,0 @@ -import bcrypt from "bcrypt" -import { decodeUrlToken, generateAccessToken, generateRefreshToken, sendRefreshToken } from "../../jwt.js" -import { sendError } from "h3" -import { getUserByEmail, getUserByUserName, sanitizeUserForFrontend, updateUser } from "~~/server/services/userService.js" -import { createSession } from "~~/server/services/sessionService.js" -import User from "~~/server/db/entities/User.js" - -export default defineEventHandler(async (event) => { - const body = await readBody(event) - var username: string = body.username - const password: string = body.password - const oldpassword: string = body.oldpassword - const token: string = body.token - var credentialValid = false - - if (!((token && password) || (password && username && oldpassword))) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Ivalid params' - })) - } - - if (token) { - const data = decodeUrlToken(token) - username = data?.username - credentialValid = true - } - - const user = await getUserByUserName(username) as User - if (!user) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Username is invalid' - })) - } - - if (!credentialValid && oldpassword) - credentialValid = await bcrypt.compare(oldpassword, user.password) - - if (!credentialValid) { - return sendError(event, createError({ - statusCode: 400, - statusMessage: 'Username or password is invalid' - })) - } - - const salt = bcrypt.genSaltSync(5); - user.password = bcrypt.hashSync(password, salt); - await updateUser(user); - - const accessToken = generateAccessToken(user) - const refreshToken = generateRefreshToken(user) - await createSession( refreshToken, user.id ) - sendRefreshToken(event, refreshToken) - - return { - access_token: accessToken, user: sanitizeUserForFrontend(user) - } -}) \ No newline at end of file diff --git a/server/api/auth/refresh.get.ts b/server/api/auth/refresh.get.ts deleted file mode 100644 index 6eb64d9..0000000 --- a/server/api/auth/refresh.get.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { sendError } from "h3" -import { sendRefreshToken } from "~~/server/jwt" -import { decodeRefreshToken, generateAccessToken, deleteRefreshToken } from "~~/server/jwt"; -import { getSessionByToken, removeOldSessions } from "~~/server/services/sessionService"; -import { getUserById, sanitizeUserForFrontend } from "~~/server/services/userService"; - -export default defineEventHandler(async (event) => { - const refreshToken = getCookie(event, "refresh_token") as string - - if (!refreshToken) { - return sendError(event, createError({ - statusCode: 401, - statusMessage: 'No Refresh token' - })) - } - - const session = await getSessionByToken(refreshToken) - - if (!session) { - return sendError(event, createError({ - statusCode: 401, - statusMessage: 'Refresh token is invalid' - })) - } - - const token = decodeRefreshToken(refreshToken) - if (!token) { - const days = 14 - const priorByDays = new Date(Date.now() - days * 24 * 60 * 60 * 1000) - await removeOldSessions(priorByDays) - deleteRefreshToken(event) - - return sendError(event, createError({ - statusCode: 403, - statusMessage: 'Session expired' - })) - } - - try { - const accessToken = generateAccessToken(token.userId) - const user = sanitizeUserForFrontend(await getUserById(token.userId)) - user.token = accessToken - return { access_token: accessToken, user } - - } catch (error) { - return sendError(event, createError({ - statusCode: 500, - statusMessage: 'Something went wrong' - })) - } -}); \ No newline at end of file diff --git a/server/api/auth/usertoken.ts b/server/api/auth/usertoken.ts deleted file mode 100644 index 5a89642..0000000 --- a/server/api/auth/usertoken.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createUserWithToken } from "~~/server/services/userService"; - -export default defineEventHandler( async (event) => { - const query = getQuery(event); - const user = await createUserWithToken({ username: query.username as string},query.type as string); - return user.token -}); - diff --git a/server/api/count.ts b/server/api/count.ts deleted file mode 100644 index 511ebef..0000000 --- a/server/api/count.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Not } from "typeorm"; -import getDataSource from "../db/dbsigleton"; -import Episode from "../db/entities/Episode"; -import Serie from "../db/entities/Serie"; - -export default defineEventHandler(async (event) => { - const query = getQuery(event); - const q = {} - if (query.hasOwnProperty('excludeId')) { - const id = query['excludeId'] - q['id'] = Not(id) - } - if (query.hasOwnProperty('id')) { - q['id'] = query.id - } - if (query.hasOwnProperty('slug')) { - q['slug'] = query.slug - } - return getDataSource().then(async (db) => { - var result = 0; - if (query.hasOwnProperty('serie')) - result = await db.manager.countBy(Serie, q); - else - result = await db.manager.countBy(Episode, q); - return result; - }); -}); diff --git a/server/api/enums.post.ts b/server/api/enums.post.ts deleted file mode 100644 index 622b32b..0000000 --- a/server/api/enums.post.ts +++ /dev/null @@ -1,14 +0,0 @@ -import getDataSource from "../db/dbsigleton"; -import { getEnumerator } from "../db/entities/Enumerator"; -import { returnCodeReject, returnCodeResolve } from "../returncode"; - -export default defineEventHandler(async (event) => { - try { - const body = await readBody(event); - const db = await getDataSource(); - db.manager.save(body.map((item) => getEnumerator(item))); - return returnCodeResolve(201, "Enum saved"); - } catch (err) { - return returnCodeReject(500, err.message); - } -}); diff --git a/server/api/enums.ts b/server/api/enums.ts deleted file mode 100644 index 0112ee3..0000000 --- a/server/api/enums.ts +++ /dev/null @@ -1,9 +0,0 @@ -import getDataSource from "~~/server/db/dbsigleton"; -import Enumerator from "~~/server/db/entities/Enumerator"; - -export default defineEventHandler( (event) => { - return getDataSource().then( async (db) => { - const repo = db.getRepository(Enumerator) - return await repo.find() - }) -}); \ No newline at end of file diff --git a/server/api/episode.delete.ts b/server/api/episode.delete.ts deleted file mode 100644 index 34e032c..0000000 --- a/server/api/episode.delete.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Episode from "~~/server/db/entities/Episode"; -import getDataSource from "~~/server/db/dbsigleton"; -import { returnCode } from "~~/server/returncode"; - -export default defineEventHandler(async (event) => { - return new Promise(async (resolve, reject) => { - const body = await readBody(event); - var id = body["id"]; - return getDataSource().then(async (db) => { - const repository = db.getRepository(Episode) - const result = await repository.softDelete(id) - if (result && result.affected == 1) - resolve(returnCode(201, "Episode deleted successfully")); - else reject(returnCode(500, "Some uncaught internal error")); - }); - }); -}); diff --git a/server/api/episode.post.ts b/server/api/episode.post.ts deleted file mode 100644 index a1a2b65..0000000 --- a/server/api/episode.post.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { returnCodeReject, returnCodeResolve } from "../returncode"; -import { saveNewEpisode, updateEpisode } from "../services/episodeService"; -import { isUpdate } from "../services/podcastService"; -import { migrateEpisode } from "../services/wpMigrationService"; - -export default defineEventHandler(async (event) => { - try { - const data = await readBody(event); - if (isUpdate(data)) { - await updateEpisode(data); - } else { - await saveNewEpisode(data) - } - } catch (err) { - console.log(err); - return returnCodeReject(500, err.message); - } - return returnCodeResolve(201, "Podcast saved"); -}); diff --git a/server/api/episode.ts b/server/api/episode.ts deleted file mode 100644 index cf31f23..0000000 --- a/server/api/episode.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { readEpisode } from "../services/episodeService"; - -export default defineEventHandler((event) => { - const query = getQuery(event); - return readEpisode(query); -}); - diff --git a/server/api/episodemove.post.ts b/server/api/episodemove.post.ts deleted file mode 100644 index 2879788..0000000 --- a/server/api/episodemove.post.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isRegularExpressionLiteral } from "typescript"; -import { SERVER_MP3_PATH } from "~~/base/Constants"; -import { updateEpisode } from "../services/episodeService"; -import { moveFile, nuxtPath } from "../services/filesService"; -import { ContentFile } from "../../base/ContentFile" -export default defineEventHandler(async (event) => { - const { episode, podcast, serie } = await readBody(event); - if (!ContentFile.isQualifiedUrl(episode.link)) { - const file = ContentFile.getFilename(episode.link) - const newpath = SERVER_MP3_PATH + podcast.slug - await moveFile(nuxtPath(episode.link), newpath, file) - episode.link = newpath + '/' + file - } - episode.podcast = podcast - episode.serie = serie - - return await updateEpisode(episode); -}); - - diff --git a/server/api/episodes.ts b/server/api/episodes.ts deleted file mode 100644 index 39db97e..0000000 --- a/server/api/episodes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { readEpisodes } from "../services/episodeService"; - -export default defineEventHandler((event) => { - return readEpisodes(); -}); - diff --git a/server/api/episodewp.post.ts b/server/api/episodewp.post.ts deleted file mode 100644 index 5f668c2..0000000 --- a/server/api/episodewp.post.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { returnCodeReject, returnCodeResolve } from "../returncode"; -import { saveNewEpisode, updateEpisode } from "../services/episodeService"; -import { isUpdate } from "../services/podcastService"; -import { migrateEpisode } from "../services/wpMigrationService"; - -export default defineEventHandler(async (event) => { - try { - const data = await readBody(event); - await migrateEpisode(data.episode, (data.podcastId?data.podcastId:0)) - } catch (err) { - console.log(err); - return returnCodeReject(500, err.message); - } - return returnCodeResolve(201, "Podcast saved"); -}); diff --git a/server/api/fetchfile.post.ts b/server/api/fetchfile.post.ts deleted file mode 100644 index d91416f..0000000 --- a/server/api/fetchfile.post.ts +++ /dev/null @@ -1,45 +0,0 @@ -import fs from 'fs'; -import { IFetchFileResult } from '../../base/types/IFetchFileResult'; -import { createDir, nuxtPath } from '../services/filesService'; - -export default defineEventHandler(async (event): Promise => { - const body = await readBody(event); - return new Promise(async (resolve, reject) => { - const path = nuxtPath(body.newpath); - const altpath = nuxtPath(body.altpath); - const filename = path + '/' + body.newfile; - const alternative = altpath + '/' + body.newfile; - if (fs.existsSync(filename)) - resolve({ - status: 423, - message: 'File already exists', - path: body.newpath + '/' + body.newfile, - }); - else if ( - body.altpath && - body.altpath.length > 0 && - fs.existsSync(alternative) - ) { - resolve({ - status: 423, - message: 'File already exists', - path: body.altpath + '/' + body.newfile, - }); - } else { - try { - createDir(path); - const data: Blob = await $fetch(body.orgurl, { responseType: 'blob' }); - const buffer = Buffer.from(await data.arrayBuffer()); - fs.writeFileSync(filename, buffer); - } catch (err) { - resolve({ status: 400, message: err.message }); - return; - } - resolve({ - status: 201, - message: 'File fetched', - path: body.newpath + '/' + body.newfile, - }); - } - }); -}); diff --git a/server/api/files.ts b/server/api/files.ts deleted file mode 100644 index 2846da9..0000000 --- a/server/api/files.ts +++ /dev/null @@ -1,17 +0,0 @@ -import fs from 'fs' -import {sendError} from 'h3' -import { DATA_PATH } from '~~/base/Constants'; -import { getPathForMediaFile } from '../services/episodeService'; -import registerEvent from '../umami'; - -export default defineEventHandler( async (event) => { - const query = getQuery(event); - var path = '' - if (query.path) - path = DATA_PATH + query.path; - else if (query.name) { - path = DATA_PATH + await getPathForMediaFile(query.name) - } else - sendError(event, createError("No path or filename provided")) - return sendStream(event,fs.createReadStream(path)) -}) \ No newline at end of file diff --git a/server/api/fromarchive.post.ts b/server/api/fromarchive.post.ts deleted file mode 100644 index bb7e2e2..0000000 --- a/server/api/fromarchive.post.ts +++ /dev/null @@ -1,22 +0,0 @@ -import fs from 'fs' -import {sendError} from 'h3' -import { ARCHIV_PATH } from '~~/base/Constants'; -import { findFile } from '../findFilePath'; -import { IFetchFileResult } from '../../base/types/IFetchFileResult'; -import { copyFile } from '../services/filesService'; - - -export default defineEventHandler( async (event) => { - const body = await readBody(event); - const path = findFile(body.name, ARCHIV_PATH) - if (path && path.length>0) { - var newpath = body.serverPath + body.slug; - if (copyFile(path, newpath, body.name)) - return { - status: 201, - message: 'File fetched', - path: newpath + '/' + body.name, - } - } - return sendError(event, createError({statusCode: 500, statusMessage: "File not found"})) -}) \ No newline at end of file diff --git a/server/api/generaterss.ts b/server/api/generaterss.ts deleted file mode 100644 index 7e662f3..0000000 --- a/server/api/generaterss.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { defineEventHandler, sendError, createError, getQuery } from "h3"; -import { useSettings } from "~~/composables/settingsdata"; -import { generateFeed, readPodcast } from "../services/podcastService"; -import getSettings from "../settings"; - -export default defineEventHandler( async (event) => { - try { - const query = getQuery(event); - const podcast = await readPodcast(query) - const settings = getSettings(); - - generateFeed(podcast, settings.baseUrl) - return true - } catch (error) { - sendError(event, createError({ - statusCode: 500, - statusMessage: "Internal server error"})) - } -}); - \ No newline at end of file diff --git a/server/api/menu.ts b/server/api/menu.ts deleted file mode 100644 index 87d940d..0000000 --- a/server/api/menu.ts +++ /dev/null @@ -1,5 +0,0 @@ -import getMenu from '../menu'; - -export default defineEventHandler( async (event) => { - return getMenu() -}) \ No newline at end of file diff --git a/server/api/podcast.delete.ts b/server/api/podcast.delete.ts deleted file mode 100644 index ce1b21b..0000000 --- a/server/api/podcast.delete.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Podcast from "~~/server/db/entities/Podcast"; -import getDataSource from "~~/server/db/dbsigleton"; -import { returnCode } from "~~/server/returncode"; - -export default defineEventHandler(async (event) => { - return new Promise(async (resolve, reject) => { - const body = await readBody(event); - var id = body["id"]; - return getDataSource().then(async (db) => { - const result = await db.manager.softDelete(Podcast, id); - if (result && result.affected == 1) - resolve(returnCode(201, "Podcast deleted successfully")); - else reject(returnCode(500, "Some uncaught internal error")); - }); - }); -}); diff --git a/server/api/podcast.post.ts b/server/api/podcast.post.ts deleted file mode 100644 index 344e98d..0000000 --- a/server/api/podcast.post.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - isUpdate, - saveNewPodcast, - updatePodcast, -} from '~~/server/services/podcastService'; -import { returnCodeReject, returnCodeResolve } from '~~/server/returncode'; -import IPodcast from '~~/base/types/IPodcast'; - -export default defineEventHandler(async (event) => { - try { - const data: IPodcast = await readBody(event); - if (isUpdate(data)) { - await updatePodcast(data); - } else { - await saveNewPodcast(data); - } - } catch (err) { - return returnCodeReject(500, err.message); - } - return returnCodeResolve(201, 'Podcast saved'); -}); diff --git a/server/api/podcast.ts b/server/api/podcast.ts deleted file mode 100644 index 8183556..0000000 --- a/server/api/podcast.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineEventHandler, getQuery } from "h3"; -import { readPodcast } from "../services/podcastService"; - -export default defineEventHandler((event) => { - const query = getQuery(event); - return readPodcast(query); - }); - \ No newline at end of file diff --git a/server/api/podcasts.ts b/server/api/podcasts.ts deleted file mode 100644 index 4631141..0000000 --- a/server/api/podcasts.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineEventHandler } from "h3"; -import IPodcast from "../../base/types/IPodcast"; -import Episode from "../db/entities/Episode"; -import Podcast from "../db/entities/Podcast"; -import { readPodcast, readPodcasts } from "../services/podcastService"; - - -function mockreadPodcasts(query) : Array { - const d = new Date(); - var ret = new Podcast() - ret.id = 1, - ret.cover_file = ""; - ret.title = "Title test "; - ret.author = "Author"; - ret.slug = "test"; - ret.category_id = 7; - ret.language_id = 1; - ret.episodes = [ new Episode() ]; - ret.episodes[0].title = "Episode Title"; - ret.episodes[0].pubdate = d; - var ret2 = { ...ret } - ret2.title = "Title 2 test "; - ret2.slug = "test2"; - return [ret, ret2] -} - -export default defineEventHandler((event) => { - return readPodcasts(); - //return mockreadPodcasts(query) - }); - \ No newline at end of file diff --git a/server/api/serie.delete.ts b/server/api/serie.delete.ts deleted file mode 100644 index 3b1aa49..0000000 --- a/server/api/serie.delete.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Serie from "~~/server/db/entities/Serie"; -import getDataSource from "~~/server/db/dbsigleton"; -import { returnCode } from "~~/server/returncode"; - -export default defineEventHandler(async (event) => { - return new Promise(async (resolve, reject) => { - const body = await readBody(event); - var id = body["id"]; - return getDataSource().then(async (db) => { - const result = await db.manager.softDelete(Serie, id); - if (result && result.affected == 1) - resolve(returnCode(201, "Serie deleted successfully")); - else reject(returnCode(500, "Some uncaught internal error")); - }); - }); -}); diff --git a/server/api/serie.post.ts b/server/api/serie.post.ts deleted file mode 100644 index b058493..0000000 --- a/server/api/serie.post.ts +++ /dev/null @@ -1,21 +0,0 @@ -import getDataSource from "../db/dbsigleton"; -import { getSerie } from "../db/entities/Serie"; -import { returnCodeReject, returnCodeResolve } from "../returncode"; -import { setLastAndFirst, updateSerie } from "../services/serieService"; - -export default defineEventHandler(async (event) => { - try { - const body = await readBody(event); - if (body.hasOwnProperty('id')) { - if (body.hasOwnProperty('title')) { - updateSerie(body) - return returnCodeResolve(201, "Saved series successfully"); - } else { - return setLastAndFirst(body.id) - } - } else - return returnCodeReject(501, "nd data"); - } catch (err) { - return returnCodeReject(500, err.message); - } -}); diff --git a/server/api/serie.ts b/server/api/serie.ts deleted file mode 100644 index 785fa3d..0000000 --- a/server/api/serie.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineEventHandler, getQuery } from "h3"; -import { readSerie } from "../services/serieService"; - -export default defineEventHandler((event) => { - const query = getQuery(event); - return readSerie(query); - }); - \ No newline at end of file diff --git a/server/api/series.post.ts b/server/api/series.post.ts deleted file mode 100644 index 84ff641..0000000 --- a/server/api/series.post.ts +++ /dev/null @@ -1,14 +0,0 @@ -import getDataSource from "../db/dbsigleton"; -import { getSerie } from "../db/entities/Serie"; -import { returnCodeReject, returnCodeResolve } from "../returncode"; - -export default defineEventHandler(async (event) => { - try { - const body = await readBody(event); - const db = await getDataSource(); - db.manager.save(body.map((item) => getSerie(item))); - return returnCodeResolve(201, "Saved series successfully"); - } catch (err) { - return returnCodeReject(500, err.message); - } -}); diff --git a/server/api/series.ts b/server/api/series.ts deleted file mode 100644 index 75c2b10..0000000 --- a/server/api/series.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { readSeries } from "../services/serieService"; - -export default defineEventHandler((event) => { - const query = getQuery(event); - return readSeries(query); -}); - diff --git a/server/api/settings.ts b/server/api/settings.ts deleted file mode 100644 index 3e2cc6a..0000000 --- a/server/api/settings.ts +++ /dev/null @@ -1,7 +0,0 @@ -import fs from 'fs' -import { DATA_PATH } from '~~/base/Constants'; -import getSettings from '../settings'; - -export default defineEventHandler( async (event) => { - return getSettings() -}) \ No newline at end of file diff --git a/server/api/upload.post.ts b/server/api/upload.post.ts deleted file mode 100644 index 8d611b7..0000000 --- a/server/api/upload.post.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createDir, moveFile } from '~~/server/services/filesService'; -import { returnCode } from '~~/server/returncode'; -import { UPLOAD_TEMP_PATH } from '~~/base/Constants'; -import Busboy from 'busboy' -import fs from 'fs' - -async function paseFormdata(req: any) { - return new Promise((resolve) => { - var filedata = {} as any - const fields = {} as any - const busboy = Busboy({ headers: req.headers }) - busboy.on('file', (name: string, file: any, info: any) => { - var { filename, encoding, mimeType } = info - if (filename.length==0) - filename = "tmp" - const path = UPLOAD_TEMP_PATH+filename; - var ws = fs.createWriteStream(path) - filedata = { - fieldname: name, - path, - filename, - encoding, - mimeType - } - file.on('close',()=>{ - ws.close() - }) - file.on('data',(chunk)=>{ - ws.write(chunk) - }) - }) - busboy.on('field', (name : string, value: object, info: any) => { - fields[name] = value - }) - busboy.on('close', () => { - resolve({ uploaded: filedata.path, path: fields.path, filename: fields.filename }) - }) - req.pipe(busboy) - return - }) -} - -export default defineEventHandler( async (event) => { - try { - createDir(UPLOAD_TEMP_PATH) - const { filename, path, uploaded } = await paseFormdata(event.node.req) - if (moveFile(uploaded, path, filename)) - return returnCode(201) - } catch (err) { - sendError(event, createError({statusCode: 500, statusMessage: err.message})); - } - sendError(event, createError({statusCode: 500, statusMessage: 'No File found'})); -}); diff --git a/server/db/data/podcasts.sqlite b/server/db/data/podcasts.sqlite deleted file mode 100644 index 8107dcf..0000000 Binary files a/server/db/data/podcasts.sqlite and /dev/null differ diff --git a/server/db/datasource.ts b/server/db/datasource.ts deleted file mode 100644 index 28147d5..0000000 --- a/server/db/datasource.ts +++ /dev/null @@ -1,18 +0,0 @@ -import "reflect-metadata"; -import { DataSource } from "typeorm"; -import Podcast from "./entities/Podcast"; -import Enumerator from "./entities/Enumerator"; -import Episode from "./entities/Episode"; -import Serie from "./entities/Serie"; -import User from "./entities/User"; -import Session from "./entities/Session"; - -var defaultFilename = "public/s/data/podcasts.sqlite"; - -export const AppDataSource = new DataSource({ - type: "sqlite", - database: defaultFilename, - entities: [Podcast, Serie, Episode, Enumerator, User, Session], - logging: false, - synchronize: true, - }); diff --git a/server/db/dbsigleton.ts b/server/db/dbsigleton.ts deleted file mode 100644 index 046a325..0000000 --- a/server/db/dbsigleton.ts +++ /dev/null @@ -1,34 +0,0 @@ -import "reflect-metadata"; -import { DataSource } from "typeorm"; -import Enumerator from "./entities/Enumerator"; -import { fillDefaultEnums, addAdmin } from "./initdata"; -import { AppDataSource } from "./datasource"; - -var dataSource: DataSource = null - -export function setTestDataSource(TstDataSource: DataSource) { - dataSource = TstDataSource -} - -export default async function getDataSource(): Promise { - if (!dataSource) - dataSource = AppDataSource; - if (dataSource.isInitialized) return dataSource; - else { - console.log("init db") - await dataSource.initialize(); - console.log("db initialized") - await dataSource.runMigrations(); - console.log("db migrated") - const german = await dataSource.manager.findOneBy(Enumerator, { - shorttext: "de-DE", - }); - if (!german) { - console.log("init data") - await fillDefaultEnums(dataSource); - } - addAdmin(dataSource); - return dataSource; - } -} - diff --git a/server/db/entities/Podcast.ts b/server/db/entities/Podcast.ts deleted file mode 100644 index 0f0550e..0000000 --- a/server/db/entities/Podcast.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - BaseEntity, - Column, - Entity, - OneToMany, - PrimaryGeneratedColumn, - CreateDateColumn, - DeleteDateColumn, - UpdateDateColumn, - ManyToMany, - JoinTable, -} from 'typeorm'; -import IPodcast, { emptyIPodcastFactory } from '../../../base/types/IPodcast'; -import Episode, { getEpisode } from './Episode'; -import Serie, { getSerie } from './Serie'; - -export function initPodcast(podcast: Podcast): IPodcast { - setPodcast(podcast, emptyIPodcastFactory()); - return podcast; -} - -export function setPodcast(podcast: Podcast, from: IPodcast): Podcast { - if (!from) return podcast; - if ('id' in from) podcast.id = from.id; - podcast.cover_file = from.cover_file; - podcast.title = from.title; - podcast.slug = from.slug; - podcast.subtitle = from.subtitle; - podcast.author = from.author; - podcast.summary = from.summary; - podcast.description = from.description; - podcast.language_id = from.language_id; - podcast.category_id = from.category_id; - podcast.type_id = from.type_id; - podcast.explicit = from.explicit; - podcast.link = from.link; - podcast.draft = from.draft; - podcast.copyright = from.copyright; - podcast.owner_name = from.owner_name; - podcast.owner_email = from.owner_email; - if (from.hasOwnProperty('state')) podcast.state = from.state; - if (from.hasOwnProperty('lastbuild')) podcast.lastbuild = from.lastbuild; - if (from.hasOwnProperty('external_id')) - podcast.external_id = from.external_id; - if (from.hasOwnProperty('apple_url')) - podcast.apple_url = from.apple_url; - if (from.hasOwnProperty('spotify_url')) - podcast.spotify_url = from.spotify_url; - if (from.hasOwnProperty('google_url')) - podcast.google_url = from.google_url; - if (from.hasOwnProperty('stitcher_url')) - podcast.stitcher_url = from.stitcher_url; - return podcast; -} - - export function getPodcast(from : Podcast): Podcast { - var podcast = setPodcast(new Podcast(), from); - if (from.episodes) { - podcast.episodes = from.episodes.map((element) => getEpisode(element)); - } - return podcast; -} - -@Entity() -export default class Podcast extends BaseEntity implements IPodcast { - @PrimaryGeneratedColumn() - id: number; - - @Column('text') - cover_file: string; - - @Column('text') - title: string; - - @Column('text') - slug: string; - - @Column('text') - subtitle: string; - - @Column('text') - author: string; - - @Column('text') - summary: string; - - @Column('text') - description: string; - - @Column('int') - language_id: number; - - @Column('int') - category_id: number; - - @Column('int') - type_id: number; - - @Column('boolean') - explicit: boolean; - - @Column('text') - link: string; - - @Column('text') - copyright: string; - - @Column('text') - owner_name: string; - - @Column('text') - owner_email: string; - - @Column('text') - lastbuild: string; - - @Column('int') - state: number; - - @Column('int') - external_id: number; - - @Column({ - type: 'boolean', - default: false, - }) - draft: boolean; - - @Column({ - type: 'text', - default: '', - }) - apple_url: string; - - @Column({ - type: 'text', - default: '', - }) - spotify_url: string; - - @Column({ - type: 'text', - default: '', - }) - google_url: string; - - @Column({ - type: 'text', - default: '', - }) - stitcher_url: string; - - @OneToMany(() => Episode, (episode) => episode.podcast, { - cascade: true, - onDelete: 'CASCADE', - }) - episodes: Episode[]; - - @CreateDateColumn({ type: 'datetime' }) - public createdAt: Date; - - @UpdateDateColumn({ type: 'datetime' }) - public updatedAt: Date; - - @DeleteDateColumn({ type: 'datetime' }) - public deletedAt: Date; -} diff --git a/server/db/entities/User.ts b/server/db/entities/User.ts deleted file mode 100644 index 0368050..0000000 --- a/server/db/entities/User.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; -import { IUser } from "../../../base/types/IUser"; -import Session from "./Session"; - -export function getUser( userdata: IUser ) { - var user = new User(); - if (userdata.id>0) - user.id = userdata.id; - user.username = userdata.username; - user.email = userdata.email; - if (userdata.hasOwnProperty('email')) - user.email = userdata.email; - else - user.email = ""; - if (userdata.hasOwnProperty('name')) - user.name = userdata.name; - else - user.name = ""; - if (userdata.hasOwnProperty('password')) user.password = userdata.password; - if (userdata.hasOwnProperty('token')) user.token = userdata.token; - return user; -} - -@Entity() -export default class User extends BaseEntity implements IUser{ - - @PrimaryGeneratedColumn() - id: number; - - @Column("text") - username: string; - - @Column("text") - name: string; - - @Column("text") - email: string; - - @Column({ type: "text", nullable: true }) - password: string; - - @Column({ type: "text", nullable: true }) - token: string; - - @OneToMany(() => Session, (session) => session.user, { - cascade: true, - onDelete: "CASCADE", - }) - sessions: Session[]; -} diff --git a/server/findFilePath.ts b/server/findFilePath.ts deleted file mode 100644 index 3a5e808..0000000 --- a/server/findFilePath.ts +++ /dev/null @@ -1,19 +0,0 @@ -import fs from 'fs' -import path from 'path' - -export function findFile(name: string, path: string) : string { - const dirs = [path] - while (dirs.length>0) { - const currentDir = dirs.pop() - const items = fs.readdirSync(currentDir); - for (const item of items) { - if (!(fs.lstatSync(`${currentDir}/${item}`)).isDirectory()) { - if (item===name) - return `${currentDir}/${item}` - } else - dirs.push(`${currentDir}/${item}`) - } - } - return "" -} - diff --git a/server/jwt.ts b/server/jwt.ts deleted file mode 100644 index 709de66..0000000 --- a/server/jwt.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { H3Event } from "h3" -import jwt from "jsonwebtoken" -import { IUser } from "~~/base/types/IUser" -import getSecSettings from "./security" - -export const generateAccessToken = (user: IUser) => { - - return jwt.sign({ userId: user.id }, getSecSettings().JWT_ACCESS_TOKEN_SECRET, { - expiresIn: '10m' - }) -} - -export const generateRefreshToken = (user: IUser) => { - - return jwt.sign({ userId: user.id }, getSecSettings().JWT_REFRESH_TOKEN_SECRET, { - expiresIn: '2d' - }) -} - -export const generateUrlToken = (username: string, purpose: string, expiry: string) => { - - return jwt.sign({ username, purpose }, getSecSettings().JWT_URL_TOKEN_SECRET, { - expiresIn: expiry - }) -} - -export const decodeUrlToken = (token: string) : Object | null => { - try { - return jwt.verify(token, getSecSettings().JWT_URL_TOKEN_SECRET) - } catch (error) { - return null - } -} - -export const decodeRefreshToken = (token: string) => { - try { - return jwt.verify(token, getSecSettings().JWT_REFRESH_TOKEN_SECRET) - } catch (error) { - return null - } -} - -export const decodeAccessToken = (token: string) => { - try { - return jwt.verify(token, getSecSettings().JWT_ACCESS_TOKEN_SECRET) - } catch (error) { - return null - } -} - -export const sendRefreshToken = (event: H3Event, token: string) => { - setCookie(event, "refresh_token", token, { - httpOnly: true, - sameSite: true - }) -} - -export const deleteRefreshToken = (event: H3Event) => { - deleteCookie(event, "refresh_token" ) -} - diff --git a/server/menu.ts b/server/menu.ts deleted file mode 100644 index 7db17bf..0000000 --- a/server/menu.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from 'fs' -import { DATA_PATH } from '~~/base/Constants'; -export default function getSettings() { - const data = fs.readFileSync(DATA_PATH + '/menu.json',{encoding:'utf8', flag:'r'}); - return JSON.parse(data); -} diff --git a/server/middleware/authentication.ts b/server/middleware/authentication.ts deleted file mode 100644 index 304a1b1..0000000 --- a/server/middleware/authentication.ts +++ /dev/null @@ -1,56 +0,0 @@ -import UrlPattern from "url-pattern" -import { sendError } from "h3" -import { decodeAccessToken, decodeRefreshToken, decodeUrlToken } from "../jwt" -import { getUserById } from "../services/userService" -import { d } from "vitest/dist/index-ea17aa0c" - -export default defineEventHandler(async (event) => { - const endpoints = [ - '/api/auth/user', - ] - const login = new UrlPattern('/api/auth/login') - - const isHandledByThisMiddleware = - (event.node.req.method != 'GET' && !login.match(event.node.req.url)) || - endpoints.some(endopoint => { - const pattern = new UrlPattern(endopoint) - return pattern.match(event.node.req.url) - }) - - if (!isHandledByThisMiddleware) { - return - } - - var token = event.node.req.headers['authorization']?.split(' ')[1] - var decoded = decodeAccessToken(token) - - if (!decoded) { - const regex = /refresh_token=(.*?)($|;|,(?! ))/ - var cookie = event.node.req.headers['cookie'] as string - if (!cookie) - cookie = event.node.req.headers['Cookie'] as string - const match = cookie?.match(regex) - decoded = decodeRefreshToken((match?.length>1?match[1]:"")) - } - if (!decoded) { - const { token } = await readBody(event); - if (token) - decoded = decodeUrlToken(token) - if (!decoded) - return sendError(event, createError({ - statusCode: 401, - statusMessage: 'Unauthorized' - })) - } - - try { - const userId = decoded.userId - - const user = await getUserById(userId) - - event.context.auth = { user } - } catch (error) { - return - } - -}) \ No newline at end of file diff --git a/server/returncode.ts b/server/returncode.ts deleted file mode 100644 index 07374c5..0000000 --- a/server/returncode.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const returnCode = function ( - statuscode: number, - textmessage: string -): Object { - return { - status: statuscode, - message: textmessage, - }; -}; - -export const returnCodeReject = function ( - statuscode: number, - textmessage: string -): Promise { - return Promise.reject({ - status: statuscode, - message: textmessage, - }); -}; -export const returnCodeResolve = function ( - statuscode: number, - textmessage: string -): Promise { - return Promise.resolve({ - status: statuscode, - message: textmessage, - }); -}; diff --git a/server/security.ts b/server/security.ts deleted file mode 100644 index 7be8a10..0000000 --- a/server/security.ts +++ /dev/null @@ -1,22 +0,0 @@ -import fs from 'fs' - -var settings = undefined as any -export default function getSecSettings() { - if (!settings) { - try { - const data = fs.readFileSync('settings.json',{encoding:'utf8', flag:'r'}); - settings = JSON.parse(data); - } catch { - settings = { - "JWT_ACCESS_TOKEN_SECRET": "CiHbNFoNGhFZjybkKRQidWRnJdKjdfgcHRTT58jvNTbjaBKCKBwpyCL2LBHsXq77dPHBtmHxuJZCfcnB5S3Mzg4fDmxfC8CEEdq", - "JWT_REFRESH_TOKEN_SECRET": "N3ozby3DqjT7Zoe94jk4i8p3iedfgpUG9qrRaGzpyEGdHjb9hmEfZmfCgif3jepjk5ECHsfxzEkv53mxexMQoFSyta3oL76Fsr2", - "JWT_URL_TOKEN_SECRET": "gdQD3NPk38HTvw5T3B2CzrPyRCdJkpdf8NcM4Dg3wucFoqGp8GGB5Dm84nauFaTTyjkWH58mqJKLK6j6pKheFZ8h7j6NNK3uuG", - "ADMIN_USER": "admin", - "ADMIN_PASSWORD": "password" - - } - console.log("WARNING: You should create security tokens for your installation") - } - } - return settings -} \ No newline at end of file diff --git a/server/services/episodeService.ts b/server/services/episodeService.ts deleted file mode 100644 index 51bbd39..0000000 --- a/server/services/episodeService.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Like } from "typeorm"; -import IEpisode from "~~/base/types/IEpisode"; -import getDataSource from "../db/dbsigleton"; -import Episode, { getEpisode } from "../db/entities/Episode"; -import { getPodcast } from "../db/entities/Podcast"; -import { getSerie } from "../db/entities/Serie"; -import writeTags from "../tagId3"; -import { setLastAndFirst } from "./serieService"; - -export const readEpisodes = async function (): Promise> { - const db = await getDataSource(); - const repo = db.getRepository(Episode); - return repo.find(); -} - -export const readEpisode = async function (query): Promise { - const db = await getDataSource(); - const repo = db.getRepository(Episode); - var tmpQuery = { - where: query, - relations: ["podcast", "serie"], - }; - var result:Array = await repo.find(tmpQuery); - return result.pop() -} - -export const getPathForMediaFile = async function (filename: string): Promise { - const db = await getDataSource(); - const repo = db.getRepository(Episode); - var tmpQuery = { - where: { - link: Like("%"+filename) - } - }; - var result = await repo.findOneOrFail(tmpQuery); - return result.link -} - -export const saveNewEpisode = async function (episodeObject): Promise { - var episode = getEpisode(episodeObject); - if ("podcast" in episodeObject) { - var podcast = getPodcast(episodeObject.podcast); - episode.podcast = podcast; - } - if ("serie" in episodeObject) { - var serie = getSerie(episodeObject.serie); - episode.serie = serie; - } - const db = await getDataSource(); - await db.manager.save(episode); - writeTags(episode) - return episode; -}; - -export const updateEpisode = async function (episodeObject) { - var episode = episodeObject; - if ("podcast" in episodeObject) { - var podcast = getPodcast(episodeObject.podcast); - episode.podcast = podcast; - } - if ("serie" in episodeObject) { - var serie = getSerie(episodeObject.serie); - episode.serie = serie; - } - const db = await getDataSource(); - const result = await db.manager.update(Episode, episodeObject.id, episode); - if (episode.serie) - setLastAndFirst(episode.serie.id) - return result -}; \ No newline at end of file diff --git a/server/services/filesService.ts b/server/services/filesService.ts deleted file mode 100644 index 6b46d04..0000000 --- a/server/services/filesService.ts +++ /dev/null @@ -1,29 +0,0 @@ - -import fs from 'fs'; -import { DATA_PATH } from '~~/base/Constants'; - -export const nuxtPath = (path: string) : string => { - return DATA_PATH + path; -}; - -export const createDir = (dir: string) : void => { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } -}; - -export const moveFile = function (fullpath: string, newpath : string, filename: string) { - var dir = nuxtPath(newpath); - createDir(dir); - const target_path = dir + '/' + filename; - fs.renameSync(fullpath, target_path); - return true; -}; - -export const copyFile = function (fullpath: string, newpath : string, filename: string) { - var dir = nuxtPath(newpath); - createDir(dir); - const target_path = dir + '/' + filename; - fs.copyFileSync(fullpath, target_path); - return true; -}; \ No newline at end of file diff --git a/server/services/podcastService.ts b/server/services/podcastService.ts deleted file mode 100644 index f67dd64..0000000 --- a/server/services/podcastService.ts +++ /dev/null @@ -1,69 +0,0 @@ -import fs from 'fs'; -import getDataSource from '~~/server/db/dbsigleton'; -import Podcast, { setPodcast } from '~~/server/db/entities/Podcast'; -import IPodcast from '~~/base/types/IPodcast'; -import { FEED_SLUG } from '~~/base/Constants'; -import { FindManyOptions } from 'typeorm'; -import Enumerator from '../db/entities/Enumerator'; -import Enumerations from '~~/base/Enumerations'; -import { generateRss } from '~~/base/RssGenerator'; -import { createDir, nuxtPath } from './filesService'; - -export const readPodcasts = async function (): Promise> { - const db = await getDataSource(); - const repo = db.getRepository(Podcast); - return await repo.find({ - order: { - updatedAt: 'DESC' - }}); -}; - -export const readPodcast = async function (query: Partial): Promise { - const db = await getDataSource(); - const repo = db.getRepository(Podcast); - var tmpQuery : FindManyOptions = { - where: query, - relations: ['episodes'], - }; - var result: Array = await repo.find(tmpQuery); - return result.pop() as IPodcast -}; - -export const generateFeed = async (podcast: IPodcast, baseUrl: string) => { - const db = await getDataSource(); - const repo = db.getRepository(Enumerator); - const enums = await repo.find(); - const enumFuncs = { - getLanguage: (id:number) => Enumerations.byIdOne(Enumerations.languages(enums), id), - getGenre: (id:number) => Enumerations.byIdOne(Enumerations.podcastGenres(enums), id), - getType: (id:number) => Enumerations.byIdOne(Enumerations.podcastTypes(enums), id), - } - const xml = generateRss(podcast, baseUrl, enumFuncs) - var dir = nuxtPath(FEED_SLUG); - createDir(dir) - const target_file = dir + podcast.slug+".xml" - fs.writeFileSync(target_file, xml) -} - - -export const isUpdate = function (podcastObject: IPodcast): Boolean { - if (!podcastObject) return false; - var isupdate: Boolean = 'id' in podcastObject; - if (isupdate) isupdate = podcastObject.id as number > 0; - return isupdate; -}; - -export const saveNewPodcast = async function ( - podcastObject: IPodcast -): Promise { - var podcast = setPodcast(new Podcast(), podcastObject); - const db = await getDataSource(); - await db.manager.save(podcast); - return podcast; -}; - -export const updatePodcast = async function (podcastObject : IPodcast) { - const db = await getDataSource(); - podcastObject.updatedAt = new Date(); - await db.manager.update(Podcast, podcastObject.id, podcastObject); -}; diff --git a/server/services/serieService.ts b/server/services/serieService.ts deleted file mode 100644 index 966d587..0000000 --- a/server/services/serieService.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Not, IsNull } from "typeorm"; -import ISerie from "~~/base/types/ISerie"; -import getDataSource from "../db/dbsigleton"; -import Episode from "../db/entities/Episode"; -import Serie, { getSerie } from "../db/entities/Serie"; - -export const readSeries = async function (query: Partial): Promise> { - const db = await getDataSource(); - var tmpQuery = { - where: { - - }, - order: { - lastEpisode: 'DESC' - }, - relations: ['episodes'], - }; - if (query.empty!=='true') - tmpQuery.where.firstEpisode = Not(IsNull()) - - const repo = db.getRepository(Serie); - return repo.find(tmpQuery); -} - -export const readSerie = async function (query: Partial): Promise { - const db = await getDataSource(); - const repo = db.getRepository(Serie); - var tmpQuery = { - where: query, - relations: ['episodes'], - }; - var result:Array = await repo.find(tmpQuery); - return result.pop() as ISerie -} - -export const saveNewSerie = async function (SerieObject: ISerie): Promise { - var Serie = getSerie(SerieObject); - const db = await getDataSource(); - await db.manager.save(Serie); - return Serie; -}; - -export const updateSerie = async function (SerieObject: ISerie) { - var serie = getSerie(SerieObject); - const db = await getDataSource(); - return await db.manager.update(Serie, SerieObject.id, serie); -}; - -export const setLastAndFirst = async ( id: number ) : string => { - const serie = await readSerie({ id: id }) - var minmax = serie.episodes?.reduce( (minmax, episode) => { - const d = new Date(episode.pubdate) - if (d< minmax.min) - minmax.min = d - if (d > minmax.max) - minmax.max = d - return minmax - }, { min: new Date(), max: new Date(1900)}) - serie.lastEpisode = minmax?.max - serie.firstEpisode = minmax?.min - updateSerie(serie as Serie) - return serie.slug -} diff --git a/server/services/sessionService.ts b/server/services/sessionService.ts deleted file mode 100644 index 2a93107..0000000 --- a/server/services/sessionService.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { getUserById, sanitizeUserForFrontend } from '~~/server/services/userService'; -import ISession from '~~/base/types/ISession'; -import { IUser } from '~~/base/types/IUser'; -import getDataSource from '../db/dbsigleton'; -import Session, { getSession } from '../db/entities/Session'; -import { generateAccessToken } from '../jwt'; -import {MoreThan} from 'typeorm'; - -export async function createSession( refreshToken: string, userId: number ) : Promise { - const user = await getUserById(userId) - const session = getSession({ userId, refreshToken }); - const db = await getDataSource(); - await db.manager.save(session); - return session as ISession; -} - -export async function readSession(query): Promise { - const db = await getDataSource(); - const repo = db.getRepository(Session); - var tmpQuery = { - where: query, - relations: ["user"], - }; - var result:Array = await repo.find(tmpQuery); - return result.pop() -} - -export const removeSession = async (token: string) => { - const db = await getDataSource(); - const repo = db.getRepository(Session); - var tmpQuery = { refreshToken: token } - const result = await repo.delete(tmpQuery) -} -export const removeSessions = async (userId: number) => { - const db = await getDataSource(); - const repo = db.getRepository(Session); - var tmpQuery = { userId } - const result = await repo.delete(tmpQuery) -} -export const removeOldSessions = async (date: Date) => { - const db = await getDataSource(); - const repo = db.getRepository(Session); - var tmpQuery = { updatedAt: MoreThan(date) } - const result = await repo.delete(tmpQuery) -} - -export async function getSessionByToken(token: string): Promise { - return await readSession({refreshToken: token}) -} - -export async function makeSession(user: IUser, event: CompatibilityEvent): Promise { - const authToken = generateAccessToken(user) - const refreshToken = generateAccessToken(user) - const session = await createSession({ authToken, userId: user.id }) - const userId = session.userId - - if (userId) { - setCookie(event, 'auth_token', authToken, { path: '/', httpOnly: true, sameSite: true }) - return getUserBySessionToken(authToken) - } - - throw Error('Error Creating Session') -} - -export async function getUserBySessionToken(authToken: string): Promise { - const session = await getSessionByAuthToken(authToken) - if (!session) - return null - return sanitizeUserForFrontend(session.user) -} - -export async function checkAuthentication(authToken: string, maxAgeInMin) : Promise { - const session = await readSession({authToken: authToken}) - if (!session) - return false; - if (maxAgeInMin<1) - return true; - const creation = new Date(session.createdAt); - const age = Date.now().valueOf() - creation.valueOf(); - return (age/60000) < maxAgeInMin; -} diff --git a/server/services/userService.ts b/server/services/userService.ts deleted file mode 100644 index 4695411..0000000 --- a/server/services/userService.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { I } from 'vitest/dist/global-fe52f84b'; -import { INVITE_TIME, INVITE_TOKEN, PWRESET_TIME } from '~~/base/Constants'; -import { IUser } from '~~/base/types/IUser'; -import getDataSource from '../db/dbsigleton'; -import User, { getUser } from '../db/entities/User'; -import { generateUrlToken } from '../jwt'; - -export async function getUserByEmail(email: string): Promise { - return readUser( { email: email } ); -} - -export async function getUserByUserName(username: string): Promise { - return readUser( { username: username } ); -} - -export async function getUserById( id: number ): Promise { - return readUser( { id: id } ); -} - -export async function readUser(query): Promise { - const db = await getDataSource(); - const repo = db.getRepository(User); - var tmpQuery = { - where: query, - relations: ["sessions"], - }; - var result:Array = await repo.find(tmpQuery); - return result.pop() -} - -export async function createUser(data: IUser) : Promise { - const user = getUser(data); - const db = await getDataSource(); - await db.manager.save(user); - return user; -} - -export async function updateUser(data: User) { - const db = await getDataSource(); - await db.manager.save(data); -} - - -export async function createUserWithToken(data: IUser, type: string) : Promise { - var user = await readUser({ username: data.username }) as User - if (!user) - user = getUser(data); - user.token = generateUrlToken( data.username, type, (type==INVITE_TOKEN?INVITE_TIME:PWRESET_TIME) ) - const db = await getDataSource(); - await db.manager.save(user); - return user; -} - -export function sanitizeUserForFrontend(user: IUser | undefined): IUser { - - if (!user) { - return user - } - - delete user.password - delete user.sessions - - return user -} - diff --git a/server/services/wpMigrationService.ts b/server/services/wpMigrationService.ts deleted file mode 100644 index 2bf3dcc..0000000 --- a/server/services/wpMigrationService.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { e } from 'vitest/dist/index-ea17aa0c'; -import IEpisode from '../../base/types/IEpisode'; -import getDataSource from '../db/dbsigleton'; -import Episode, { getEpisode, joinEpisodePodcastAndSerie } from '../db/entities/Episode'; -import Podcast from '../db/entities/Podcast'; -import Serie from '../db/entities/Serie'; - -export async function migrateEpisode(episode: IEpisode, podcastId: number) { - const db = await getDataSource(); - const serieRepo = db.getRepository(Serie); - const podcastRepo = db.getRepository(Podcast); - - const getPodcast = async () => { - if (podcastId>0) - return await podcastRepo.findOne({ - where: { id: podcastId }, - }); - else - return await podcastRepo.findOne({ - where: { external_id: episode.ext_podcast_id }, - }); - } - - const podcast = await getPodcast() - const serie = await serieRepo.findOne({ - where: { external_id: episode.ext_series_id }, - relations: ['episodes'] - }); - var episodeToSave = getEpisode(episode); - - joinEpisodePodcastAndSerie(episodeToSave,podcast,serie) - - await db.manager.save(episodeToSave); - if (serie) - await db.manager.save(serie); - if (podcast) - await db.manager.save(podcast); -} diff --git a/server/settings.ts b/server/settings.ts deleted file mode 100644 index 290baa3..0000000 --- a/server/settings.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from 'fs' -import { DATA_PATH } from '~~/base/Constants'; -export default function getSettings() { - const data = fs.readFileSync(DATA_PATH + '/app.settings.json',{encoding:'utf8', flag:'r'}); - return JSON.parse(data); -} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..b9ed69c --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/server/wpepisode.ts b/server/wpepisode.ts deleted file mode 100644 index 82960c7..0000000 --- a/server/wpepisode.ts +++ /dev/null @@ -1,25 +0,0 @@ -import getDataSource from "../dbsigleton"; -import Episode from "../entities/Episode"; -import Podcast from "../entities/Podcast"; -import Serie from "../entities/Serie"; - -export const migrateEpisodes = async (episodes: Array) => { - const db = await getDataSource(); - const serieRepo = db.getRepository(Serie); - const podcastRepo = db.getRepository(Podcast); - const podcast = await podcastRepo.findOne({ - where: { external_id: episodes[0].ext_podcast_id }, - }); - episodes.forEach(async (episode) => { - const serie = await serieRepo.findOne({ - where: { external_id: episode.ext_series_id }, - }); - episode.podcast = podcast; - episode.serie = serie; - db.manager.save(episode); - if (serie.podcast == undefined) { - serie.podcast = podcast; - db.manager.save(serie); - } - }); -}; diff --git a/startbe.sh b/startbe.sh new file mode 100755 index 0000000..8a678f7 --- /dev/null +++ b/startbe.sh @@ -0,0 +1,24 @@ +#!/usr/bin/bash +function clean_up() +{ + echo "Exiting..." + kill $bePID + exit +} +# Trigger cleanup on CTRL + C +trap clean_up SIGINT EXIT + +cd backend +yarn +yarn start & +bePID=$! +CHECKBE="" +i=0 +while [ -z "${CHECKBE}" -a $i -lt 20 ]; +do + CHECKBE=`curl -s http://localhost:3003/api/podcasts -I | grep 200` + sleep 2s + let "(i++)" +done + +trap clean_up ERR diff --git a/tests/e2e/app.test.ts b/tests/e2e/app.test.ts index ba1e17e..b290937 100644 --- a/tests/e2e/app.test.ts +++ b/tests/e2e/app.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest' import { setup, $fetch, url } from '@nuxt/test-utils-edge' - +const API_BASE = "/" describe('App', async () => { await setup({ server: true }) it('my test', async () => { - const html = await $fetch('/podcasts') + const html = await $fetch( API_BASE + '/podcasts') expect(html).toContain("Podcasts") }) }) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 322f3fd..a746f2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,4 @@ { - "extends": "./.nuxt/tsconfig.json", - "compilerOptions": { - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "sourceMap": true, - "types": ["@types/node", "vitest/importMeta"], //jest and vue?? - } -} \ No newline at end of file + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +} diff --git a/version.ts b/version.ts index 238b6d1..7ed3aef 100644 --- a/version.ts +++ b/version.ts @@ -1,3 +1,3 @@ -export const VERSION ="0.0.0"; -export const BUILDTIME="" -export const REVISION="" \ No newline at end of file +export const VERSION = "4" +export const REVISION = "0" +export const BUILDTIME = "today" \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 09adb25..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - globals: true, - deps: { - inline: [ - "typeorm", "@nuxt/test-utils-edge" - - ] - } - }, -})