From 42084627687c6308a26dca12243ab9969a433bc2 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Mon, 14 Oct 2019 10:40:40 +0200 Subject: Add local server --- src/server/.editorconfig | 13 ++ src/server/.eslintrc.js | 22 ++ src/server/.gitattributes | 2 + src/server/.gitignore | 19 ++ src/server/README.md | 50 +++++ src/server/ace | 21 ++ .../app/Controllers/Http/DashboardController.js | 38 ++++ .../app/Controllers/Http/RecipeController.js | 207 ++++++++++++++++++ .../app/Controllers/Http/ServiceController.js | 211 ++++++++++++++++++ .../app/Controllers/Http/StaticController.js | 224 +++++++++++++++++++ src/server/app/Controllers/Http/UserController.js | 231 ++++++++++++++++++++ .../app/Controllers/Http/WorkspaceController.js | 148 +++++++++++++ src/server/app/Exceptions/Handler.js | 47 ++++ .../app/Middleware/ConvertEmptyStringsToNull.js | 16 ++ src/server/app/Models/Recipe.js | 8 + src/server/app/Models/Service.js | 8 + src/server/app/Models/Token.js | 8 + src/server/app/Models/Traits/NoTimestamp.js | 15 ++ src/server/app/Models/User.js | 8 + src/server/app/Models/Workspace.js | 8 + src/server/config/app.js | 242 +++++++++++++++++++++ src/server/config/auth.js | 93 ++++++++ src/server/config/bodyParser.js | 156 +++++++++++++ src/server/config/cors.js | 86 ++++++++ src/server/config/database.js | 87 ++++++++ src/server/config/drive.js | 45 ++++ src/server/config/hash.js | 48 ++++ src/server/config/session.js | 98 +++++++++ src/server/config/shield.js | 144 ++++++++++++ src/server/database/factory.js | 20 ++ src/server/database/ferdi.sqlite | Bin 0 -> 36864 bytes .../migrations/1566385379883_service_schema.js | 22 ++ .../migrations/1566554231482_recipe_schema.js | 21 ++ .../migrations/1566554359294_workspace_schema.js | 23 ++ src/server/database/template.sqlite | Bin 0 -> 36864 bytes src/server/env.ini | 16 ++ src/server/logo.png | Bin 0 -> 340668 bytes src/server/public/css/main.css | 69 ++++++ src/server/public/css/vanilla.css | 138 ++++++++++++ src/server/public/js/new.js | 24 ++ src/server/resources/views/layouts/main.edge | 18 ++ src/server/resources/views/others/import.edge | 19 ++ src/server/resources/views/others/index.edge | 38 ++++ src/server/resources/views/others/new.edge | 40 ++++ src/server/start.js | 40 ++++ src/server/start/app.js | 62 ++++++ src/server/start/kernel.js | 56 +++++ src/server/start/routes.js | 74 +++++++ 48 files changed, 2983 insertions(+) create mode 100644 src/server/.editorconfig create mode 100644 src/server/.eslintrc.js create mode 100644 src/server/.gitattributes create mode 100644 src/server/.gitignore create mode 100644 src/server/README.md create mode 100644 src/server/ace create mode 100644 src/server/app/Controllers/Http/DashboardController.js create mode 100644 src/server/app/Controllers/Http/RecipeController.js create mode 100644 src/server/app/Controllers/Http/ServiceController.js create mode 100644 src/server/app/Controllers/Http/StaticController.js create mode 100644 src/server/app/Controllers/Http/UserController.js create mode 100644 src/server/app/Controllers/Http/WorkspaceController.js create mode 100644 src/server/app/Exceptions/Handler.js create mode 100644 src/server/app/Middleware/ConvertEmptyStringsToNull.js create mode 100644 src/server/app/Models/Recipe.js create mode 100644 src/server/app/Models/Service.js create mode 100644 src/server/app/Models/Token.js create mode 100644 src/server/app/Models/Traits/NoTimestamp.js create mode 100644 src/server/app/Models/User.js create mode 100644 src/server/app/Models/Workspace.js create mode 100644 src/server/config/app.js create mode 100644 src/server/config/auth.js create mode 100644 src/server/config/bodyParser.js create mode 100644 src/server/config/cors.js create mode 100644 src/server/config/database.js create mode 100644 src/server/config/drive.js create mode 100644 src/server/config/hash.js create mode 100644 src/server/config/session.js create mode 100644 src/server/config/shield.js create mode 100644 src/server/database/factory.js create mode 100644 src/server/database/ferdi.sqlite create mode 100644 src/server/database/migrations/1566385379883_service_schema.js create mode 100644 src/server/database/migrations/1566554231482_recipe_schema.js create mode 100644 src/server/database/migrations/1566554359294_workspace_schema.js create mode 100644 src/server/database/template.sqlite create mode 100644 src/server/env.ini create mode 100644 src/server/logo.png create mode 100644 src/server/public/css/main.css create mode 100644 src/server/public/css/vanilla.css create mode 100644 src/server/public/js/new.js create mode 100644 src/server/resources/views/layouts/main.edge create mode 100644 src/server/resources/views/others/import.edge create mode 100644 src/server/resources/views/others/index.edge create mode 100644 src/server/resources/views/others/new.edge create mode 100644 src/server/start.js create mode 100644 src/server/start/app.js create mode 100644 src/server/start/kernel.js create mode 100644 src/server/start/routes.js (limited to 'src/server') diff --git a/src/server/.editorconfig b/src/server/.editorconfig new file mode 100644 index 000000000..914223976 --- /dev/null +++ b/src/server/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_size = 2 +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/src/server/.eslintrc.js b/src/server/.eslintrc.js new file mode 100644 index 000000000..d02f4890d --- /dev/null +++ b/src/server/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + env: { + commonjs: true, + es6: true, + node: true, + }, + extends: [ + 'airbnb-base', + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + use: 'readonly' + }, + parserOptions: { + ecmaVersion: 2018, + }, + rules: { + "class-methods-use-this": 'off', + "no-restricted-syntax": 'off', + }, +}; diff --git a/src/server/.gitattributes b/src/server/.gitattributes new file mode 100644 index 000000000..dfe077042 --- /dev/null +++ b/src/server/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/src/server/.gitignore b/src/server/.gitignore new file mode 100644 index 000000000..d84ffadd4 --- /dev/null +++ b/src/server/.gitignore @@ -0,0 +1,19 @@ +# Node modules +node_modules + +# Adonis directory for storing tmp files +tmp + +# Environment variables, never commit this file +.env + +# The development sqlite file +database/development.sqlite +database/adonis.sqlite + +# Uploaded recipes +recipes/ + +.DS_Store +public/terms.html +public/privacy.html diff --git a/src/server/README.md b/src/server/README.md new file mode 100644 index 000000000..833d9643e --- /dev/null +++ b/src/server/README.md @@ -0,0 +1,50 @@ +

+ +

+ +# ferdi-internal-server +Internal Ferdi Server used for storing settings without logging into an external server. + +npm i @adonisjs/ace @adonisjs/auth @adonisjs/bodyparser @adonisjs/cors @adonisjs/drive @adonisjs/fold @adonisjs/framework @adonisjs/ignitor @adonisjs/lucid @adonisjs/session @adonisjs/shield @adonisjs/validator atob btoa fs-extra node-fetch sqlite3 uuid targz + +### Manual setup +1. Clone this repository +2. Install the [AdonisJS CLI](https://adonisjs.com/) +3. Run the database migrations with + ```js + adonis migration:run + ``` +4. Start the server with + ```js + adonis serve --dev + ``` + +## Configuration +franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's settings, ferdi-server has the following custom settings: +- `IS_CREATION_ENABLED` (`true` or `false`, default: `true`): Whether to enable the [creation of custom recipes](#creating-and-using-custom-recipes) +- `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-server can: + - Show the full Franz recipe library instead of only custom recipes + - Import Franz accounts + +## Importing your Franz account +ferdi-server allows you to import your full Franz account, including all its settings. + +To import your Franz account, open `http://localhost:45569/import` in your browser and login using your Franz account details. ferdi-server will create a new user with the same credentials and copy your Franz settings, services and workspaces. + +## Creating and using custom recipes +ferdi-server allows to extends the Franz recipe catalogue with custom Ferdi recipes. + +For documentation on how to create a recipe, please visit [the official guide by Franz](https://github.com/meetfranz/plugins/blob/master/docs/integration.md). + +To add your recipe to ferdi-server, open `http://localhost:45569/new` in your browser. You can now define the following settings: +- `Author`: Author who created the recipe +- `Name`: Name for your new service. Can contain spaces and unicode characters +- `Service ID`: Unique ID for this recipe. Does not contain spaces or special characters (e.g. `google-drive`) +- `Link to PNG/SVG image`: Direct link to a 1024x1024 PNG image and SVG that is used as a logo inside the store. Please use jsDelivr when using a file uploaded to GitHub as raw.githubusercontent files won't load +- `Recipe files`: Recipe files that you created using the [Franz recipe creation guide](https://github.com/meetfranz/plugins/blob/master/docs/integration.md). Please do *not* package your files beforehand - upload the raw files (you can drag and drop multiple files). ferdi-server will automatically package and store the recipe in the right format. Please also do not drag and drop or select the whole folder, select the individual files. + +### Listing custom recipes +Inside Ferdi, searching for `ferdi:custom` will list all your custom recipes. + +## License +ferdi-server is licensed under the MIT License diff --git a/src/server/ace b/src/server/ace new file mode 100644 index 000000000..42f8f10d1 --- /dev/null +++ b/src/server/ace @@ -0,0 +1,21 @@ +'use strict' + +/* +|-------------------------------------------------------------------------- +| Ace Commands +|-------------------------------------------------------------------------- +| +| The ace file is just a regular Javascript file but with no extension. You +| can call `node ace` followed by the command name and it just works. +| +| Also you can use `adonis` followed by the command name, since the adonis +| global proxies all the ace commands. +| +*/ + +const { Ignitor } = require('@adonisjs/ignitor') + +new Ignitor(require('@adonisjs/fold')) + .appRoot(__dirname) + .fireAce() + .catch(console.error) diff --git a/src/server/app/Controllers/Http/DashboardController.js b/src/server/app/Controllers/Http/DashboardController.js new file mode 100644 index 000000000..69af16227 --- /dev/null +++ b/src/server/app/Controllers/Http/DashboardController.js @@ -0,0 +1,38 @@ +class DashboardController { + async data({ + auth, + view, + }) { + const general = auth.user; + const services = (await auth.user.services().fetch()).toJSON(); + const workspaces = (await auth.user.workspaces().fetch()).toJSON(); + + return view.render('dashboard.data', { + username: general.username, + mail: general.email, + created: general.created_at, + updated: general.updated_at, + services, + workspaces, + }); + } + + logout({ + auth, + response, + }) { + auth.authenticator('session').logout(); + return response.redirect('/user/login'); + } + + delete({ + auth, + response, + }) { + auth.user.delete(); + auth.authenticator('session').logout(); + return response.redirect('/user/login'); + } +} + +module.exports = DashboardController; diff --git a/src/server/app/Controllers/Http/RecipeController.js b/src/server/app/Controllers/Http/RecipeController.js new file mode 100644 index 000000000..5ed21122a --- /dev/null +++ b/src/server/app/Controllers/Http/RecipeController.js @@ -0,0 +1,207 @@ + +const Recipe = use('App/Models/Recipe'); +const Helpers = use('Helpers'); +const Drive = use('Drive'); +const { + validateAll, +} = use('Validator'); +const Env = use('Env'); + +const fetch = require('node-fetch'); +const targz = require('targz'); +const path = require('path'); +const fs = require('fs-extra'); + +const compress = (src, dest) => new Promise((resolve, reject) => { + targz.compress({ + src, + dest, + }, (err) => { + if (err) { + reject(err); + } else { + resolve(dest); + } + }); +}); + +class RecipeController { + // List official and custom recipes + async list({ + response, + }) { + const officialRecipes = JSON.parse(await (await fetch('https://api.franzinfra.com/v1/recipes')).text()); + const customRecipesArray = (await Recipe.all()).rows; + const customRecipes = customRecipesArray.map(recipe => ({ + id: recipe.recipeId, + name: recipe.name, + ...JSON.parse(recipe.data), + })); + + const recipes = [ + ...officialRecipes, + ...customRecipes, + ]; + + return response.send(recipes); + } + + // Create a new recipe using the new.html page + async create({ + request, + response, + }) { + // Check if recipe creation is enabled + if (Env.get('IS_CREATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq + return response.send('This server doesn\'t allow the creation of new recipes.'); + } + + // Validate user input + const validation = await validateAll(request.all(), { + name: 'required|string', + id: 'required|unique:recipes,recipeId', + author: 'required|accepted', + png: 'required|url', + svg: 'required|url', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + const data = request.all(); + + if (!data.id) { + return response.send('Please provide an ID'); + } + + // Check for invalid characters + if (/\.{1,}/.test(data.id) || /\/{1,}/.test(data.id)) { + return response.send('Invalid recipe name. Your recipe name may not contain "." or "/"'); + } + + // Clear temporary recipe folder + await fs.emptyDir(Helpers.tmpPath('recipe')); + + // Move uploaded files to temporary path + const files = request.file('files'); + await files.moveAll(Helpers.tmpPath('recipe')); + + // Compress files to .tar.gz file + const source = Helpers.tmpPath('recipe'); + const destination = path.join(Helpers.appRoot(), `/recipes/${data.id}.tar.gz`); + + compress( + source, + destination, + ); + + // Create recipe in db + await Recipe.create({ + name: data.name, + recipeId: data.id, + data: JSON.stringify({ + author: data.author, + featured: false, + version: '1.0.0', + icons: { + png: data.png, + svg: data.svg, + }, + }), + }); + + return response.send('Created new recipe'); + } + + // Search official and custom recipes + async search({ + request, + response, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + needle: 'required', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Please provide a needle', + messages: validation.messages(), + status: 401, + }); + } + + const needle = request.input('needle'); + + // Get results + let results; + + if (needle === 'ferdi:custom') { + const dbResults = (await Recipe.all()).toJSON(); + results = dbResults.map(recipe => ({ + id: recipe.recipeId, + name: recipe.name, + ...JSON.parse(recipe.data), + })); + } else { + let remoteResults = []; + if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq + remoteResults = JSON.parse(await (await fetch(`https://api.franzinfra.com/v1/recipes/search?needle=${encodeURIComponent(needle)}`)).text()); + } + const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON(); + const localResults = localResultsArray.map(recipe => ({ + id: recipe.recipeId, + name: recipe.name, + ...JSON.parse(recipe.data), + })); + + results = [ + ...localResults, + ...remoteResults || [], + ]; + } + + return response.send(results); + } + + // Download a recipe + async download({ + response, + params, + }) { + // Validate user input + const validation = await validateAll(params, { + recipe: 'required|accepted', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Please provide a recipe ID', + messages: validation.messages(), + status: 401, + }); + } + + const service = params.recipe; + + // Check for invalid characters + if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) { + return response.send('Invalid recipe name'); + } + + // Check if recipe exists in recipes folder + if (await Drive.exists(`${service}.tar.gz`)) { + return response.send(await Drive.get(`${service}.tar.gz`)); + } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq + return response.redirect(`https://api.franzinfra.com/v1/recipes/download/${service}`); + } + return response.status(400).send({ + message: 'Recipe not found', + code: 'recipe-not-found', + }); + } +} + +module.exports = RecipeController; diff --git a/src/server/app/Controllers/Http/ServiceController.js b/src/server/app/Controllers/Http/ServiceController.js new file mode 100644 index 000000000..ea7035ca1 --- /dev/null +++ b/src/server/app/Controllers/Http/ServiceController.js @@ -0,0 +1,211 @@ +const Service = use('App/Models/Service'); +const { + validateAll, +} = use('Validator'); + +const uuid = require('uuid/v4'); + +class ServiceController { + // Create a new service for user + async create({ + request, + response, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + name: 'required|string', + recipeId: 'required', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + const data = request.all(); + + // Get new, unused uuid + let serviceId; + do { + serviceId = uuid(); + } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop + + await Service.create({ + serviceId, + name: data.name, + recipeId: data.recipeId, + settings: JSON.stringify(data), + }); + + return response.send({ + data: { + userId: 1, + id: serviceId, + isEnabled: true, + isNotificationEnabled: true, + isBadgeEnabled: true, + isMuted: false, + isDarkModeEnabled: '', + spellcheckerLanguage: '', + order: 1, + customRecipe: false, + hasCustomIcon: false, + workspaces: [], + iconUrl: null, + ...data, + }, + status: ['created'], + }); + } + + // List all services a user has created + async list({ + response, + }) { + const services = (await Service.all()).rows; + // Convert to array with all data Franz wants + const servicesArray = services.map(service => ({ + customRecipe: false, + hasCustomIcon: false, + isBadgeEnabled: true, + isDarkModeEnabled: '', + isEnabled: true, + isMuted: false, + isNotificationEnabled: true, + order: 1, + spellcheckerLanguage: '', + workspaces: [], + iconUrl: null, + ...JSON.parse(service.settings), + id: service.serviceId, + name: service.name, + recipeId: service.recipeId, + userId: 1, + })); + + return response.send(servicesArray); + } + + async edit({ + request, + response, + params, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + name: 'required', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + const data = request.all(); + const { + id, + } = params; + + // Get current settings from db + const serviceData = (await Service.query() + .where('serviceId', id).fetch()).rows[0]; + + const settings = { + ...JSON.parse(serviceData.settings), + ...data, + }; + + // Update data in database + await (Service.query() + .where('serviceId', id)).update({ + name: data.name, + settings: JSON.stringify(settings), + }); + + // Get updated row + const service = (await Service.query() + .where('serviceId', id).fetch()).rows[0]; + + return response.send({ + id: service.serviceId, + name: data.name, + ...settings, + userId: 1, + }); + } + + async reorder({ + request, + response, + }) { + const data = request.all(); + + for (const service of Object.keys(data)) { + // Get current settings from db + const serviceData = (await Service.query() // eslint-disable-line no-await-in-loop + .where('serviceId', service).fetch()).rows[0]; + + const settings = { + ...JSON.parse(serviceData.settings), + order: data[service], + }; + + // Update data in database + await (Service.query() // eslint-disable-line no-await-in-loop + .where('serviceId', service)) + .update({ + settings: JSON.stringify(settings), + }); + } + + // Get new services + const services = (await Service.all()).rows; + // Convert to array with all data Franz wants + const servicesArray = services.map(service => ({ + customRecipe: false, + hasCustomIcon: false, + isBadgeEnabled: true, + isDarkModeEnabled: '', + isEnabled: true, + isMuted: false, + isNotificationEnabled: true, + order: 1, + spellcheckerLanguage: '', + workspaces: [], + iconUrl: null, + ...JSON.parse(service.settings), + id: service.serviceId, + name: service.name, + recipeId: service.recipeId, + userId: 1, + })); + + return response.send(servicesArray); + } + + update({ + response, + }) { + return response.send([]); + } + + async delete({ + params, + response, + }) { + // Update data in database + await (Service.query() + .where('serviceId', params.id)).delete(); + + return response.send({ + message: 'Sucessfully deleted service', + status: 200, + }); + } +} + +module.exports = ServiceController; diff --git a/src/server/app/Controllers/Http/StaticController.js b/src/server/app/Controllers/Http/StaticController.js new file mode 100644 index 000000000..b16e6cb6d --- /dev/null +++ b/src/server/app/Controllers/Http/StaticController.js @@ -0,0 +1,224 @@ + +/** + * Controller for routes with static responses + */ + +class StaticController { + // Enable all features + features({ + response, + }) { + return response.send({ + needToWaitToProceed: false, + isSpellcheckerPremiumFeature: true, + isServiceProxyEnabled: true, + isServiceProxyPremiumFeature: true, + isWorkspacePremiumFeature: true, + isWorkspaceEnabled: true, + isAnnouncementsEnabled: true, + isSettingsWSEnabled: false, + isServiceLimitEnabled: false, + serviceLimitCount: 0, + isCommunityRecipesPremiumFeature: false, + }); + } + + // Return an empty array + emptyArray({ + response, + }) { + return response.send([]); + } + + // Payment plans availible + plans({ + response, + }) { + return response.send({ + month: { + id: 'franz-supporter-license', + price: 99, + }, + year: { + id: 'franz-supporter-license-year-2019', + price: 99, + }, + }); + } + + // Return list of popular recipes (copy of the response Franz's API is returning) + popularRecipes({ + response, + }) { + return response.send([{ + author: 'Stefan Malzner ', + featured: false, + id: 'slack', + name: 'Slack', + version: '1.0.4', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'whatsapp', + name: 'WhatsApp', + version: '1.0.1', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'messenger', + name: 'Messenger', + version: '1.0.6', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'telegram', + name: 'Telegram', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'gmail', + name: 'Gmail', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'skype', + name: 'Skype', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'hangouts', + name: 'Hangouts', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'discord', + name: 'Discord', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'tweetdeck', + name: 'Tweetdeck', + version: '1.0.1', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'hipchat', + name: 'HipChat', + version: '1.0.1', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'gmailinbox', + name: 'Inbox by Gmail', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'rocketchat', + name: 'Rocket.Chat', + version: '1.0.1', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg', + }, + }, { + author: 'Brian Gilbert ', + featured: false, + id: 'gitter', + name: 'Gitter', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg', + }, + }, { + author: 'Stefan Malzner ', + featured: false, + id: 'mattermost', + name: 'Mattermost', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg', + }, + }, { + author: 'Franz ', + featured: false, + id: 'toggl', + name: 'toggl', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg', + }, + }, { + author: 'Stuart Clark ', + featured: false, + id: 'twist', + name: 'twist', + version: '1.0.0', + icons: { + png: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png', + svg: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg', + }, + }]); + } + + // Show announcements + announcement({ + response, + }) { + return response.send('No announcement found.'); + } +} + +module.exports = StaticController; diff --git a/src/server/app/Controllers/Http/UserController.js b/src/server/app/Controllers/Http/UserController.js new file mode 100644 index 000000000..758250ec9 --- /dev/null +++ b/src/server/app/Controllers/Http/UserController.js @@ -0,0 +1,231 @@ +const Service = use('App/Models/Service'); +const Workspace = use('App/Models/Workspace'); +const { + validateAll, +} = use('Validator'); +const Env = use('Env'); + +const btoa = require('btoa'); +const fetch = require('node-fetch'); +const uuid = require('uuid/v4'); +const crypto = require('crypto'); + +const franzRequest = (route, method, auth) => new Promise((resolve, reject) => { + const base = 'https://api.franzinfra.com/v1/'; + const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; + + try { + fetch(base + route, { + method, + headers: { + Authorization: `Bearer ${auth}`, + 'User-Agent': user, + }, + }) + .then(data => data.json()) + .then(json => resolve(json)); + } catch (e) { + reject(); + } +}); + +class UserController { + // Register a new user + async signup({ + request, + response, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + firstname: 'required', + email: 'required|email|unique:users,email', + password: 'required', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + return response.send({ + message: 'Successfully created account', + token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M', + }); + } + + // Login using an existing user + async login({ + request, + response, + }) { + if (!request.header('Authorization')) { + return response.status(401).send({ + message: 'Please provide authorization', + status: 401, + }); + } + + return response.send({ + message: 'Successfully logged in', + token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M', + }); + } + + // Return information about the current user + async me({ + response, + }) { + return response.send({ + accountType: 'individual', + beta: false, + donor: {}, + email: '', + emailValidated: true, + features: {}, + firstname: 'Ferdi', + id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8', + isPremium: true, + isSubscriptionOwner: true, + lastname: 'Application', + locale: 'en-US', + }); + } + + + async import({ + request, + response, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + email: 'required|email|unique:users,email', + password: 'required', + }); + if (validation.fails()) { + let errorMessage = 'There was an error while trying to import your account:\n'; + for (const message of validation.messages()) { + if (message.validation === 'required') { + errorMessage += `- Please make sure to supply your ${message.field}\n`; + } else if (message.validation === 'unique') { + errorMessage += '- There is already a user with this email.\n'; + } else { + errorMessage += `${message.message}\n`; + } + } + return response.status(401).send(errorMessage); + } + + const { + email, + password, + } = request.all(); + + const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); + + if (Env.get('CONNECT_WITH_FRANZ') == 'false') { // eslint-disable-line eqeqeq + return response.send('Your account has been created but due to this server\'s configuration, we could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.'); + } + + const base = 'https://api.franzinfra.com/v1/'; + const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; + + // Try to get an authentication token + let token; + try { + const basicToken = btoa(`${email}:${hashedPassword}`); + + const rawResponse = await fetch(`${base}auth/login`, { + method: 'POST', + headers: { + Authorization: `Basic ${basicToken}`, + 'User-Agent': userAgent, + }, + }); + const content = await rawResponse.json(); + + if (!content.message || content.message !== 'Successfully logged in') { + const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again'; + return response.status(401).send(errorMessage); + } + + // eslint-disable-next-line prefer-destructuring + token = content.token; + } catch (e) { + return response.status(401).send({ + message: 'Cannot login to Franz', + error: e, + }); + } + + // Get user information + let userInf = false; + try { + userInf = await franzRequest('me', 'GET', token); + } catch (e) { + const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`; + return response.status(401).send(errorMessage); + } + if (!userInf) { + const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later'; + return response.status(401).send(errorMessage); + } + + const serviceIdTranslation = {}; + + // Import services + try { + const services = await franzRequest('me/services', 'GET', token); + + for (const service of services) { + // Get new, unused uuid + let serviceId; + do { + serviceId = uuid(); + } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop + + await Service.create({ // eslint-disable-line no-await-in-loop + serviceId, + name: service.name, + recipeId: service.recipeId, + settings: JSON.stringify(service), + }); + + serviceIdTranslation[service.id] = serviceId; + } + } catch (e) { + const errorMessage = `Could not import your services into our system.\nError: ${e}`; + return response.status(401).send(errorMessage); + } + + // Import workspaces + try { + const workspaces = await franzRequest('workspace', 'GET', token); + + for (const workspace of workspaces) { + let workspaceId; + do { + workspaceId = uuid(); + } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop + + const services = workspace.services.map(service => serviceIdTranslation[service]); + + await Workspace.create({ // eslint-disable-line no-await-in-loop + workspaceId, + name: workspace.name, + order: workspace.order, + services: JSON.stringify(services), + data: JSON.stringify({}), + }); + } + } catch (e) { + const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; + return response.status(401).send(errorMessage); + } + + return response.send('Your account has been imported. You can now use your Franz account in Ferdi.'); + } +} + +module.exports = UserController; diff --git a/src/server/app/Controllers/Http/WorkspaceController.js b/src/server/app/Controllers/Http/WorkspaceController.js new file mode 100644 index 000000000..7990b8434 --- /dev/null +++ b/src/server/app/Controllers/Http/WorkspaceController.js @@ -0,0 +1,148 @@ +const Workspace = use('App/Models/Workspace'); +const { + validateAll, +} = use('Validator'); + +const uuid = require('uuid/v4'); + +class WorkspaceController { + // Create a new workspace for user + async create({ + request, + response, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + name: 'required|alpha', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + const data = request.all(); + + // Get new, unused uuid + let workspaceId; + do { + workspaceId = uuid(); + } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop + + const order = (await Workspace.all()).rows.length; + + await Workspace.create({ + workspaceId, + name: data.name, + order, + services: JSON.stringify([]), + data: JSON.stringify(data), + }); + + return response.send({ + userId: 1, + name: data.name, + id: workspaceId, + order, + workspaces: [], + }); + } + + async edit({ + request, + response, + params, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + name: 'required|alpha', + services: 'required|array', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + const data = request.all(); + const { + id, + } = params; + + // Update data in database + await (Workspace.query() + .where('workspaceId', id)).update({ + name: data.name, + services: JSON.stringify(data.services), + }); + + // Get updated row + const workspace = (await Workspace.query() + .where('workspaceId', id).fetch()).rows[0]; + + return response.send({ + id: workspace.workspaceId, + name: data.name, + order: workspace.order, + services: data.services, + userId: 1, + }); + } + + async delete({ + request, + response, + params, + }) { + // Validate user input + const validation = await validateAll(request.all(), { + id: 'required', + }); + if (validation.fails()) { + return response.status(401).send({ + message: 'Invalid POST arguments', + messages: validation.messages(), + status: 401, + }); + } + + const { + id, + } = params; + + // Update data in database + await (Workspace.query() + .where('workspaceId', id)).delete(); + + return response.send({ + message: 'Successfully deleted workspace', + }); + } + + // List all workspaces a user has created + async list({ + response, + }) { + const workspaces = (await Workspace.all()).rows; + // Convert to array with all data Franz wants + let workspacesArray = []; + if (workspaces) { + workspacesArray = workspaces.map(workspace => ({ + id: workspace.workspaceId, + name: workspace.name, + order: workspace.order, + services: JSON.parse(workspace.services), + userId: 1, + })); + } + + + return response.send(workspacesArray); + } +} + +module.exports = WorkspaceController; diff --git a/src/server/app/Exceptions/Handler.js b/src/server/app/Exceptions/Handler.js new file mode 100644 index 000000000..cb9e10bbe --- /dev/null +++ b/src/server/app/Exceptions/Handler.js @@ -0,0 +1,47 @@ + +const BaseExceptionHandler = use('BaseExceptionHandler'); + +/** + * This class handles all exceptions thrown during + * the HTTP request lifecycle. + * + * @class ExceptionHandler + */ +class ExceptionHandler extends BaseExceptionHandler { + /** + * Handle exception thrown during the HTTP lifecycle + * + * @method handle + * + * @param {Object} error + * @param {Object} options.request + * @param {Object} options.response + * + * @return {void} + */ + async handle(error, { response }) { + if (error.name === 'ValidationException') { + return response.status(400).send('Invalid arguments'); + } if (error.name === 'InvalidSessionException') { + return response.status(401).redirect('/user/login'); + } + + return response.status(error.status).send(error.message); + } + + /** + * Report exception for logging or debugging. + * + * @method report + * + * @param {Object} error + * @param {Object} options.request + * + * @return {void} + */ + async report() { + return true; + } +} + +module.exports = ExceptionHandler; diff --git a/src/server/app/Middleware/ConvertEmptyStringsToNull.js b/src/server/app/Middleware/ConvertEmptyStringsToNull.js new file mode 100644 index 000000000..bc3079a7f --- /dev/null +++ b/src/server/app/Middleware/ConvertEmptyStringsToNull.js @@ -0,0 +1,16 @@ + +class ConvertEmptyStringsToNull { + async handle({ request }, next) { + if (Object.keys(request.body).length) { + request.body = Object.assign( + ...Object.keys(request.body).map(key => ({ + [key]: request.body[key] !== '' ? request.body[key] : null, + })), + ); + } + + await next(); + } +} + +module.exports = ConvertEmptyStringsToNull; diff --git a/src/server/app/Models/Recipe.js b/src/server/app/Models/Recipe.js new file mode 100644 index 000000000..da3618bf7 --- /dev/null +++ b/src/server/app/Models/Recipe.js @@ -0,0 +1,8 @@ + +/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ +const Model = use('Model'); + +class Recipe extends Model { +} + +module.exports = Recipe; diff --git a/src/server/app/Models/Service.js b/src/server/app/Models/Service.js new file mode 100644 index 000000000..20679feb1 --- /dev/null +++ b/src/server/app/Models/Service.js @@ -0,0 +1,8 @@ + +/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ +const Model = use('Model'); + +class Service extends Model { +} + +module.exports = Service; diff --git a/src/server/app/Models/Token.js b/src/server/app/Models/Token.js new file mode 100644 index 000000000..f6bec0852 --- /dev/null +++ b/src/server/app/Models/Token.js @@ -0,0 +1,8 @@ + +/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ +const Model = use('Model'); + +class Token extends Model { +} + +module.exports = Token; diff --git a/src/server/app/Models/Traits/NoTimestamp.js b/src/server/app/Models/Traits/NoTimestamp.js new file mode 100644 index 000000000..c647428b3 --- /dev/null +++ b/src/server/app/Models/Traits/NoTimestamp.js @@ -0,0 +1,15 @@ + +class NoTimestamp { + register(Model) { + Object.defineProperties(Model, { + createdAtColumn: { + get: () => null, + }, + updatedAtColumn: { + get: () => null, + }, + }); + } +} + +module.exports = NoTimestamp; diff --git a/src/server/app/Models/User.js b/src/server/app/Models/User.js new file mode 100644 index 000000000..9783cbe45 --- /dev/null +++ b/src/server/app/Models/User.js @@ -0,0 +1,8 @@ + +/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ +const Model = use('Model'); + +class User extends Model { +} + +module.exports = User; diff --git a/src/server/app/Models/Workspace.js b/src/server/app/Models/Workspace.js new file mode 100644 index 000000000..3b73cbf33 --- /dev/null +++ b/src/server/app/Models/Workspace.js @@ -0,0 +1,8 @@ + +/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ +const Model = use('Model'); + +class Workspace extends Model { +} + +module.exports = Workspace; diff --git a/src/server/config/app.js b/src/server/config/app.js new file mode 100644 index 000000000..7938b81df --- /dev/null +++ b/src/server/config/app.js @@ -0,0 +1,242 @@ + +/** @type {import('@adonisjs/framework/src/Env')} */ +const Env = use('Env'); + +module.exports = { + + /* + |-------------------------------------------------------------------------- + | Application Name + |-------------------------------------------------------------------------- + | + | This value is the name of your application and can used when you + | need to place the application's name in a email, view or + | other location. + | + */ + + name: Env.get('APP_NAME', 'Ferdi Internal Server'), + + /* + |-------------------------------------------------------------------------- + | App Key + |-------------------------------------------------------------------------- + | + | App key is a randomly generated 16 or 32 characters long string required + | to encrypt cookies, sessions and other sensitive data. + | + */ + appKey: Env.getOrFail('APP_KEY'), + + http: { + /* + |-------------------------------------------------------------------------- + | Allow Method Spoofing + |-------------------------------------------------------------------------- + | + | Method spoofing allows to make requests by spoofing the http verb. + | Which means you can make a GET request but instruct the server to + | treat as a POST or PUT request. If you want this feature, set the + | below value to true. + | + */ + allowMethodSpoofing: true, + + /* + |-------------------------------------------------------------------------- + | Trust Proxy + |-------------------------------------------------------------------------- + | + | Trust proxy defines whether X-Forwarded-* headers should be trusted or not. + | When your application is behind a proxy server like nginx, these values + | are set automatically and should be trusted. Apart from setting it + | to true or false Adonis supports handful or ways to allow proxy + | values. Read documentation for that. + | + */ + trustProxy: false, + + /* + |-------------------------------------------------------------------------- + | Subdomains + |-------------------------------------------------------------------------- + | + | Offset to be used for returning subdomains for a given request.For + | majority of applications it will be 2, until you have nested + | sudomains. + | cheatsheet.adonisjs.com - offset - 2 + | virk.cheatsheet.adonisjs.com - offset - 3 + | + */ + subdomainOffset: 2, + + /* + |-------------------------------------------------------------------------- + | JSONP Callback + |-------------------------------------------------------------------------- + | + | Default jsonp callback to be used when callback query string is missing + | in request url. + | + */ + jsonpCallback: 'callback', + + + /* + |-------------------------------------------------------------------------- + | Etag + |-------------------------------------------------------------------------- + | + | Set etag on all HTTP response. In order to disable for selected routes, + | you can call the `response.send` with an options object as follows. + | + | response.send('Hello', { ignoreEtag: true }) + | + */ + etag: false, + }, + + views: { + /* + |-------------------------------------------------------------------------- + | Cache Views + |-------------------------------------------------------------------------- + | + | Define whether or not to cache the compiled view. Set it to true in + | production to optimize view loading time. + | + */ + cache: Env.get('CACHE_VIEWS', true), + }, + + static: { + /* + |-------------------------------------------------------------------------- + | Dot Files + |-------------------------------------------------------------------------- + | + | Define how to treat dot files when trying to server static resources. + | By default it is set to ignore, which will pretend that dotfiles + | does not exists. + | + | Can be one of the following + | ignore, deny, allow + | + */ + dotfiles: 'ignore', + + /* + |-------------------------------------------------------------------------- + | ETag + |-------------------------------------------------------------------------- + | + | Enable or disable etag generation + | + */ + etag: true, + + /* + |-------------------------------------------------------------------------- + | Extensions + |-------------------------------------------------------------------------- + | + | Set file extension fallbacks. When set, if a file is not found, the given + | extensions will be added to the file name and search for. The first + | that exists will be served. Example: ['html', 'htm']. + | + */ + extensions: false, + }, + + locales: { + /* + |-------------------------------------------------------------------------- + | Loader + |-------------------------------------------------------------------------- + | + | The loader to be used for fetching and updating locales. Below is the + | list of available options. + | + | file, database + | + */ + loader: 'file', + + /* + |-------------------------------------------------------------------------- + | Default Locale + |-------------------------------------------------------------------------- + | + | Default locale to be used by Antl provider. You can always switch drivers + | in runtime or use the official Antl middleware to detect the driver + | based on HTTP headers/query string. + | + */ + locale: 'en', + }, + + logger: { + /* + |-------------------------------------------------------------------------- + | Transport + |-------------------------------------------------------------------------- + | + | Transport to be used for logging messages. You can have multiple + | transports using same driver. + | + | Available drivers are: `file` and `console`. + | + */ + transport: 'console', + + /* + |-------------------------------------------------------------------------- + | Console Transport + |-------------------------------------------------------------------------- + | + | Using `console` driver for logging. This driver writes to `stdout` + | and `stderr` + | + */ + console: { + driver: 'console', + name: 'adonis-app', + level: 'info', + }, + + /* + |-------------------------------------------------------------------------- + | File Transport + |-------------------------------------------------------------------------- + | + | File transport uses file driver and writes log messages for a given + | file inside `tmp` directory for your app. + | + | For a different directory, set an absolute path for the filename. + | + */ + file: { + driver: 'file', + name: 'adonis-app', + filename: 'adonis.log', + level: 'info', + }, + }, + + /* + |-------------------------------------------------------------------------- + | Generic Cookie Options + |-------------------------------------------------------------------------- + | + | The following cookie options are generic settings used by AdonisJs to create + | cookies. However, some parts of the application like `sessions` can have + | separate settings for cookies inside `config/session.js`. + | + */ + cookie: { + httpOnly: true, + sameSite: false, + path: '/', + maxAge: 7200, + }, +}; diff --git a/src/server/config/auth.js b/src/server/config/auth.js new file mode 100644 index 000000000..b831b06c6 --- /dev/null +++ b/src/server/config/auth.js @@ -0,0 +1,93 @@ + +/** @type {import('@adonisjs/framework/src/Env')} */ +const Env = use('Env'); + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Authenticator + |-------------------------------------------------------------------------- + | + | Authentication is a combination of serializer and scheme with extra + | config to define on how to authenticate a user. + | + | Available Schemes - basic, session, jwt, api + | Available Serializers - lucid, database + | + */ + authenticator: 'jwt', + + /* + |-------------------------------------------------------------------------- + | Session + |-------------------------------------------------------------------------- + | + | Session authenticator makes use of sessions to authenticate a user. + | Session authentication is always persistent. + | + */ + session: { + serializer: 'lucid', + model: 'App/Models/User', + scheme: 'session', + uid: 'email', + password: 'password', + }, + + /* + |-------------------------------------------------------------------------- + | Basic Auth + |-------------------------------------------------------------------------- + | + | The basic auth authenticator uses basic auth header to authenticate a + | user. + | + | NOTE: + | This scheme is not persistent and users are supposed to pass + | login credentials on each request. + | + */ + basic: { + serializer: 'lucid', + model: 'App/Models/User', + scheme: 'basic', + uid: 'email', + password: 'password', + }, + + /* + |-------------------------------------------------------------------------- + | Jwt + |-------------------------------------------------------------------------- + | + | The jwt authenticator works by passing a jwt token on each HTTP request + | via HTTP `Authorization` header. + | + */ + jwt: { + serializer: 'lucid', + model: 'App/Models/User', + scheme: 'jwt', + uid: 'email', + password: 'password', + options: { + secret: Env.get('APP_KEY'), + }, + }, + + /* + |-------------------------------------------------------------------------- + | Api + |-------------------------------------------------------------------------- + | + | The Api scheme makes use of API personal tokens to authenticate a user. + | + */ + api: { + serializer: 'lucid', + model: 'App/Models/User', + scheme: 'api', + uid: 'email', + password: 'password', + }, +}; diff --git a/src/server/config/bodyParser.js b/src/server/config/bodyParser.js new file mode 100644 index 000000000..c336e67d2 --- /dev/null +++ b/src/server/config/bodyParser.js @@ -0,0 +1,156 @@ + +module.exports = { + /* + |-------------------------------------------------------------------------- + | JSON Parser + |-------------------------------------------------------------------------- + | + | Below settings are applied when the request body contains a JSON payload. + | If you want body parser to ignore JSON payloads, then simply set `types` + | to an empty array. + */ + json: { + /* + |-------------------------------------------------------------------------- + | limit + |-------------------------------------------------------------------------- + | + | Defines the limit of JSON that can be sent by the client. If payload + | is over 1mb it will not be processed. + | + */ + limit: '50mb', + + /* + |-------------------------------------------------------------------------- + | strict + |-------------------------------------------------------------------------- + | + | When `strict` is set to true, body parser will only parse Arrays and + | Object. Otherwise everything parseable by `JSON.parse` is parsed. + | + */ + strict: true, + + /* + |-------------------------------------------------------------------------- + | types + |-------------------------------------------------------------------------- + | + | Which content types are processed as JSON payloads. You are free to + | add your own types here, but the request body should be parseable + | by `JSON.parse` method. + | + */ + types: [ + 'application/json', + 'application/json-patch+json', + 'application/vnd.api+json', + 'application/csp-report', + ], + }, + + /* + |-------------------------------------------------------------------------- + | Raw Parser + |-------------------------------------------------------------------------- + | + | + | + */ + raw: { + types: [ + 'text/*', + ], + }, + + /* + |-------------------------------------------------------------------------- + | Form Parser + |-------------------------------------------------------------------------- + | + | + | + */ + form: { + types: [ + 'application/x-www-form-urlencoded', + ], + }, + + /* + |-------------------------------------------------------------------------- + | Files Parser + |-------------------------------------------------------------------------- + | + | + | + */ + files: { + types: [ + 'multipart/form-data', + ], + + /* + |-------------------------------------------------------------------------- + | Max Size + |-------------------------------------------------------------------------- + | + | Below value is the max size of all the files uploaded to the server. It + | is validated even before files have been processed and hard exception + | is thrown. + | + | Consider setting a reasonable value here, otherwise people may upload GB's + | of files which will keep your server busy. + | + | Also this value is considered when `autoProcess` is set to true. + | + */ + maxSize: '20mb', + + /* + |-------------------------------------------------------------------------- + | Auto Process + |-------------------------------------------------------------------------- + | + | Whether or not to auto-process files. Since HTTP servers handle files via + | couple of specific endpoints. It is better to set this value off and + | manually process the files when required. + | + | This value can contain a boolean or an array of route patterns + | to be autoprocessed. + */ + autoProcess: true, + + /* + |-------------------------------------------------------------------------- + | Process Manually + |-------------------------------------------------------------------------- + | + | The list of routes that should not process files and instead rely on + | manual process. This list should only contain routes when autoProcess + | is to true. Otherwise everything is processed manually. + | + */ + processManually: [], + + /* + |-------------------------------------------------------------------------- + | Temporary file name + |-------------------------------------------------------------------------- + | + | Define a function, which should return a string to be used as the + | tmp file name. + | + | If not defined, Bodyparser will use `uuid` as the tmp file name. + | + | To be defined as. If you are defining the function, then do make sure + | to return a value from it. + | + | tmpFileName () { + | return 'some-unique-value' + | } + | + */ + }, +}; diff --git a/src/server/config/cors.js b/src/server/config/cors.js new file mode 100644 index 000000000..7ebbe3ffa --- /dev/null +++ b/src/server/config/cors.js @@ -0,0 +1,86 @@ + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Origin + |-------------------------------------------------------------------------- + | + | Set a list of origins to be allowed. The value can be one of the following + | + | Boolean: true - Allow current request origin + | Boolean: false - Disallow all + | String - Comma separated list of allowed origins + | Array - An array of allowed origins + | String: * - A wildcard to allow current request origin + | Function - Receives the current origin and should return one of the above values. + | + */ + origin: false, + + /* + |-------------------------------------------------------------------------- + | Methods + |-------------------------------------------------------------------------- + | + | HTTP methods to be allowed. The value can be one of the following + | + | String - Comma separated list of allowed methods + | Array - An array of allowed methods + | + */ + methods: ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'], + + /* + |-------------------------------------------------------------------------- + | Headers + |-------------------------------------------------------------------------- + | + | List of headers to be allowed via Access-Control-Request-Headers header. + | The value can be one of the following. + | + | Boolean: true - Allow current request headers + | Boolean: false - Disallow all + | String - Comma separated list of allowed headers + | Array - An array of allowed headers + | String: * - A wildcard to allow current request headers + | Function - Receives the current header and should return one of the above values. + | + */ + headers: true, + + /* + |-------------------------------------------------------------------------- + | Expose Headers + |-------------------------------------------------------------------------- + | + | A list of headers to be exposed via `Access-Control-Expose-Headers` + | header. The value can be one of the following. + | + | Boolean: false - Disallow all + | String: Comma separated list of allowed headers + | Array - An array of allowed headers + | + */ + exposeHeaders: false, + + /* + |-------------------------------------------------------------------------- + | Credentials + |-------------------------------------------------------------------------- + | + | Define Access-Control-Allow-Credentials header. It should always be a + | boolean. + | + */ + credentials: false, + + /* + |-------------------------------------------------------------------------- + | MaxAge + |-------------------------------------------------------------------------- + | + | Define Access-Control-Allow-Max-Age + | + */ + maxAge: 90, +}; diff --git a/src/server/config/database.js b/src/server/config/database.js new file mode 100644 index 000000000..86f18dac5 --- /dev/null +++ b/src/server/config/database.js @@ -0,0 +1,87 @@ + +/** @type {import('@adonisjs/framework/src/Env')} */ +const Env = use('Env'); + +// eslint-disable-next-line import/no-extraneous-dependencies +const { app } = require('electron'); +const path = require('path'); + +const dbPath = path.join(app.getPath('userData'), 'server.sqlite'); + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Default Connection + |-------------------------------------------------------------------------- + | + | Connection defines the default connection settings to be used while + | interacting with SQL databases. + | + */ + connection: Env.get('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Sqlite + |-------------------------------------------------------------------------- + | + | Sqlite is a flat file database and can be a good choice for a development + | environment. + | + | npm i --save sqlite3 + | + */ + sqlite: { + client: 'sqlite3', + connection: { + // filename: Helpers.databasePath(`${Env.get('DB_DATABASE', 'development')}.sqlite`), + filename: dbPath, + }, + useNullAsDefault: true, + debug: Env.get('DB_DEBUG', false), + }, + + /* + |-------------------------------------------------------------------------- + | MySQL + |-------------------------------------------------------------------------- + | + | Here we define connection settings for MySQL database. + | + | npm i --save mysql + | + */ + mysql: { + client: 'mysql', + connection: { + host: Env.get('DB_HOST', 'localhost'), + port: Env.get('DB_PORT', ''), + user: Env.get('DB_USER', 'root'), + password: Env.get('DB_PASSWORD', ''), + database: Env.get('DB_DATABASE', 'adonis'), + }, + debug: Env.get('DB_DEBUG', false), + }, + + /* + |-------------------------------------------------------------------------- + | PostgreSQL + |-------------------------------------------------------------------------- + | + | Here we define connection settings for PostgreSQL database. + | + | npm i --save pg + | + */ + pg: { + client: 'pg', + connection: { + host: Env.get('DB_HOST', 'localhost'), + port: Env.get('DB_PORT', ''), + user: Env.get('DB_USER', 'root'), + password: Env.get('DB_PASSWORD', ''), + database: Env.get('DB_DATABASE', 'adonis'), + }, + debug: Env.get('DB_DEBUG', false), + }, +}; diff --git a/src/server/config/drive.js b/src/server/config/drive.js new file mode 100644 index 000000000..617ce470a --- /dev/null +++ b/src/server/config/drive.js @@ -0,0 +1,45 @@ +const Env = use('Env'); + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Default disk + |-------------------------------------------------------------------------- + | + | The default disk is used when you interact with the file system without + | defining a disk name + | + */ + default: 'local', + + disks: { + /* + |-------------------------------------------------------------------------- + | Local + |-------------------------------------------------------------------------- + | + | Local disk interacts with the a local folder inside your application + | + */ + local: { + root: `${__dirname}/../recipes`, + driver: 'local', + }, + + /* + |-------------------------------------------------------------------------- + | S3 + |-------------------------------------------------------------------------- + | + | S3 disk interacts with a bucket on aws s3 + | + */ + s3: { + driver: 's3', + key: Env.get('S3_KEY'), + secret: Env.get('S3_SECRET'), + bucket: Env.get('S3_BUCKET'), + region: Env.get('S3_REGION'), + }, + }, +}; diff --git a/src/server/config/hash.js b/src/server/config/hash.js new file mode 100644 index 000000000..297c977fc --- /dev/null +++ b/src/server/config/hash.js @@ -0,0 +1,48 @@ + +/** @type {import('@adonisjs/framework/src/Env')} */ +const Env = use('Env'); + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Driver + |-------------------------------------------------------------------------- + | + | Driver to be used for hashing values. The same driver is used by the + | auth module too. + | + */ + driver: Env.get('HASH_DRIVER', 'bcrypt'), + + /* + |-------------------------------------------------------------------------- + | Bcrypt + |-------------------------------------------------------------------------- + | + | Config related to bcrypt hashing. https://www.npmjs.com/package/bcrypt + | package is used internally. + | + */ + bcrypt: { + rounds: 10, + }, + + /* + |-------------------------------------------------------------------------- + | Argon + |-------------------------------------------------------------------------- + | + | Config related to argon. https://www.npmjs.com/package/argon2 package is + | used internally. + | + | Since argon is optional, you will have to install the dependency yourself + | + |============================================================================ + | npm i argon2 + |============================================================================ + | + */ + argon: { + type: 1, + }, +}; diff --git a/src/server/config/session.js b/src/server/config/session.js new file mode 100644 index 000000000..bce28bdd9 --- /dev/null +++ b/src/server/config/session.js @@ -0,0 +1,98 @@ + +const Env = use('Env'); + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Session Driver + |-------------------------------------------------------------------------- + | + | The session driver to be used for storing session values. It can be + | cookie, file or redis. + | + | For `redis` driver, make sure to install and register `@adonisjs/redis` + | + */ + driver: Env.get('SESSION_DRIVER', 'cookie'), + + /* + |-------------------------------------------------------------------------- + | Cookie Name + |-------------------------------------------------------------------------- + | + | The name of the cookie to be used for saving session id. Session ids + | are signed and encrypted. + | + */ + cookieName: 'adonis-session', + + /* + |-------------------------------------------------------------------------- + | Clear session when browser closes + |-------------------------------------------------------------------------- + | + | If this value is true, the session cookie will be temporary and will be + | removed when browser closes. + | + */ + clearWithBrowser: true, + + /* + |-------------------------------------------------------------------------- + | Session age + |-------------------------------------------------------------------------- + | + | This value is only used when `clearWithBrowser` is set to false. The + | age must be a valid https://npmjs.org/package/ms string or should + | be in milliseconds. + | + | Valid values are: + | '2h', '10d', '5y', '2.5 hrs' + | + */ + age: '2h', + + /* + |-------------------------------------------------------------------------- + | Cookie options + |-------------------------------------------------------------------------- + | + | Cookie options defines the options to be used for setting up session + | cookie + | + */ + cookie: { + httpOnly: true, + path: '/', + sameSite: false, + }, + + /* + |-------------------------------------------------------------------------- + | Sessions location + |-------------------------------------------------------------------------- + | + | If driver is set to file, we need to define the relative location from + | the temporary path or absolute url to any location. + | + */ + file: { + location: 'sessions', + }, + + /* + |-------------------------------------------------------------------------- + | Redis config + |-------------------------------------------------------------------------- + | + | The configuration for the redis driver. + | + */ + redis: { + host: '127.0.0.1', + port: 6379, + password: null, + db: 0, + keyPrefix: '', + }, +}; diff --git a/src/server/config/shield.js b/src/server/config/shield.js new file mode 100644 index 000000000..5c1c5cd73 --- /dev/null +++ b/src/server/config/shield.js @@ -0,0 +1,144 @@ + +module.exports = { + /* + |-------------------------------------------------------------------------- + | Content Security Policy + |-------------------------------------------------------------------------- + | + | Content security policy filters out the origins not allowed to execute + | and load resources like scripts, styles and fonts. There are wide + | variety of options to choose from. + */ + csp: { + /* + |-------------------------------------------------------------------------- + | Directives + |-------------------------------------------------------------------------- + | + | All directives are defined in camelCase and here is the list of + | available directives and their possible values. + | + | https://content-security-policy.com + | + | @example + | directives: { + | defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com'] + | } + | + */ + directives: { + }, + /* + |-------------------------------------------------------------------------- + | Report only + |-------------------------------------------------------------------------- + | + | Setting `reportOnly=true` will not block the scripts from running and + | instead report them to a URL. + | + */ + reportOnly: false, + /* + |-------------------------------------------------------------------------- + | Set all headers + |-------------------------------------------------------------------------- + | + | Headers staring with `X` have been depreciated, since all major browsers + | supports the standard CSP header. So its better to disable deperciated + | headers, unless you want them to be set. + | + */ + setAllHeaders: false, + + /* + |-------------------------------------------------------------------------- + | Disable on android + |-------------------------------------------------------------------------- + | + | Certain versions of android are buggy with CSP policy. So you can set + | this value to true, to disable it for Android versions with buggy + | behavior. + | + | Here is an issue reported on a different package, but helpful to read + | if you want to know the behavior. https://github.com/helmetjs/helmet/pull/82 + | + */ + disableAndroid: true, + }, + + /* + |-------------------------------------------------------------------------- + | X-XSS-Protection + |-------------------------------------------------------------------------- + | + | X-XSS Protection saves from applications from XSS attacks. It is adopted + | by IE and later followed by some other browsers. + | + | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + | + */ + xss: { + enabled: true, + enableOnOldIE: false, + }, + + /* + |-------------------------------------------------------------------------- + | Iframe Options + |-------------------------------------------------------------------------- + | + | xframe defines whether or not your website can be embedded inside an + | iframe. Choose from one of the following options. + | @available options + | DENY, SAMEORIGIN, ALLOW-FROM http://example.com + | + | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + */ + xframe: 'DENY', + + /* + |-------------------------------------------------------------------------- + | No Sniff + |-------------------------------------------------------------------------- + | + | Browsers have a habit of sniffing content-type of a response. Which means + | files with .txt extension containing Javascript code will be executed as + | Javascript. You can disable this behavior by setting nosniff to false. + | + | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + | + */ + nosniff: true, + + /* + |-------------------------------------------------------------------------- + | No Open + |-------------------------------------------------------------------------- + | + | IE users can execute webpages in the context of your website, which is + | a serious security risk. Below option will manage this for you. + | + */ + noopen: true, + + /* + |-------------------------------------------------------------------------- + | CSRF Protection + |-------------------------------------------------------------------------- + | + | CSRF Protection adds another layer of security by making sure, actionable + | routes does have a valid token to execute an action. + | + */ + csrf: { + enable: true, + methods: ['POST', 'PUT', 'DELETE'], + filterUris: [], + cookieOptions: { + httpOnly: false, + sameSite: true, + path: '/', + maxAge: 7200, + }, + }, +}; diff --git a/src/server/database/factory.js b/src/server/database/factory.js new file mode 100644 index 000000000..550c5e6ab --- /dev/null +++ b/src/server/database/factory.js @@ -0,0 +1,20 @@ + +/* +|-------------------------------------------------------------------------- +| Factory +|-------------------------------------------------------------------------- +| +| Factories are used to define blueprints for database tables or Lucid +| models. Later you can use these blueprints to seed your database +| with dummy data. +| +*/ + +/** @type {import('@adonisjs/lucid/src/Factory')} */ +// const Factory = use('Factory') + +// Factory.blueprint('App/Models/User', (faker) => { +// return { +// username: faker.username() +// } +// }) diff --git a/src/server/database/ferdi.sqlite b/src/server/database/ferdi.sqlite new file mode 100644 index 000000000..db5425ee6 Binary files /dev/null and b/src/server/database/ferdi.sqlite differ diff --git a/src/server/database/migrations/1566385379883_service_schema.js b/src/server/database/migrations/1566385379883_service_schema.js new file mode 100644 index 000000000..1db95c19d --- /dev/null +++ b/src/server/database/migrations/1566385379883_service_schema.js @@ -0,0 +1,22 @@ + +/** @type {import('@adonisjs/lucid/src/Schema')} */ +const Schema = use('Schema'); + +class ServiceSchema extends Schema { + up() { + this.create('services', (table) => { + table.increments(); + table.string('serviceId', 80).notNullable(); + table.string('name', 80).notNullable(); + table.string('recipeId', 254).notNullable(); + table.json('settings'); + table.timestamps(); + }); + } + + down() { + this.drop('services'); + } +} + +module.exports = ServiceSchema; diff --git a/src/server/database/migrations/1566554231482_recipe_schema.js b/src/server/database/migrations/1566554231482_recipe_schema.js new file mode 100644 index 000000000..14fcb82e5 --- /dev/null +++ b/src/server/database/migrations/1566554231482_recipe_schema.js @@ -0,0 +1,21 @@ + +/** @type {import('@adonisjs/lucid/src/Schema')} */ +const Schema = use('Schema'); + +class RecipeSchema extends Schema { + up() { + this.create('recipes', (table) => { + table.increments(); + table.string('name', 80).notNullable(); + table.string('recipeId', 254).notNullable().unique(); + table.json('data'); + table.timestamps(); + }); + } + + down() { + this.drop('recipes'); + } +} + +module.exports = RecipeSchema; diff --git a/src/server/database/migrations/1566554359294_workspace_schema.js b/src/server/database/migrations/1566554359294_workspace_schema.js new file mode 100644 index 000000000..b53bbe656 --- /dev/null +++ b/src/server/database/migrations/1566554359294_workspace_schema.js @@ -0,0 +1,23 @@ + +/** @type {import('@adonisjs/lucid/src/Schema')} */ +const Schema = use('Schema'); + +class WorkspaceSchema extends Schema { + up() { + this.create('workspaces', (table) => { + table.increments(); + table.string('workspaceId', 80).notNullable().unique(); + table.string('name', 80).notNullable(); + table.integer('order'); + table.json('services'); + table.json('data'); + table.timestamps(); + }); + } + + down() { + this.drop('workspaces'); + } +} + +module.exports = WorkspaceSchema; diff --git a/src/server/database/template.sqlite b/src/server/database/template.sqlite new file mode 100644 index 000000000..db5425ee6 Binary files /dev/null and b/src/server/database/template.sqlite differ diff --git a/src/server/env.ini b/src/server/env.ini new file mode 100644 index 000000000..902e8e4c8 --- /dev/null +++ b/src/server/env.ini @@ -0,0 +1,16 @@ +HOST=127.0.0.1 +PORT=45569 +NODE_ENV=development +APP_NAME=Ferdi Internal Server +APP_URL=http://${HOST}:${PORT} +CACHE_VIEWS=false +APP_KEY=FERDIINTERNALSERVER +DB_CONNECTION=sqlite +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USER=root +DB_PASSWORD= +DB_DATABASE=ferdi +HASH_DRIVER=bcrypt +IS_CREATION_ENABLED=true +CONNECT_WITH_FRANZ=true \ No newline at end of file diff --git a/src/server/logo.png b/src/server/logo.png new file mode 100644 index 000000000..587e0b86e Binary files /dev/null and b/src/server/logo.png differ diff --git a/src/server/public/css/main.css b/src/server/public/css/main.css new file mode 100644 index 000000000..a1c5653d7 --- /dev/null +++ b/src/server/public/css/main.css @@ -0,0 +1,69 @@ +input { + margin-bottom: 1rem; + width: 100%; + padding: 0.5rem; +} + +button, .button { + display: flex; + overflow: hidden; + padding: 12px 12px; + cursor: pointer; + width: 100%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all 150ms linear; + text-align: center; + white-space: nowrap; + text-decoration: none !important; + text-transform: none; + text-transform: capitalize; + color: #fff !important; + border: 0 none; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + line-height: 1.3; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + justify-content: center; + align-items: center; + flex: 0 0 160px; + box-shadow: 2px 5px 10px #e4e4e4; + color: #FFFFFF; + background: #161616; +} + +#dropzone { + width: 100%; + height: 30vh; + background-color: #ebebeb; + + display: flex; + align-items: center; + justify-content: center; + text-align: center; + + cursor: pointer; +} + +#dropzone p { + font-size: 0.85rem; +} + +#files { + display: none; +} + +.alert { + background-color: #e7a8a6; + padding: 0.8rem; + margin-bottom: 1rem; +} + +td { + word-break: break-all; +} \ No newline at end of file diff --git a/src/server/public/css/vanilla.css b/src/server/public/css/vanilla.css new file mode 100644 index 000000000..37bc051a2 --- /dev/null +++ b/src/server/public/css/vanilla.css @@ -0,0 +1,138 @@ +/* Reset */ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +* { + box-sizing: border-box; +} + + + +/* Variables */ +:root { + --desktop-font-size: 1.3rem/1.5; + --mobile-font-size: 1.1rem/1.4; + --text-color: #2d2d2d; + --link-color: blue; + --primary-color: lightsteelblue; + --secondary-color: aliceblue; + --tertiary-color: whitesmoke; +} + + + + +/* Typography */ +body { + color: var(--text-color); + padding: 3rem; + font: var(--desktop-font-size) -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto, Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol"; +} + +h1,h2,h3,h4,h5,h6,p,blockquote,dl,img,figure { + margin: 2rem 0; +} + +h1,h2,h3,h4,h5,h6 { font-weight: bold; } +h1 { font-size: 200%; } +h2 { font-size: 150%; } +h3 { font-size: 120%; } +h4,h5,h6 { font-size: 100%; } +h5, h6 { text-transform: uppercase; } + +header h1 { border-bottom: 1px solid; } + +p { margin: 2rem 0; } + +a,a:visited { color: var(--link-color); } + +strong, time, b { font-weight: bold; } +em, dfn, i { font-style: italic; } +sub { font-size: 60%; vertical-align: bottom; } +small { font-size: 80%; } + +blockquote, q { + background: var(--secondary-color); + border-left: 10px solid var(--primary-color); + font-family: "Georgia", serif; + padding: 1rem; +} +blockquote p:first-child { margin-top: 0; } +cite { + font-family: "Georgia", serif; + font-style: italic; + font-weight: bold; +} + +kbd,code,samp,pre,var { font-family: monospace; font-weight: bold; } +code, pre { + background: var(--tertiary-color); + padding: 0.5rem 1rem; +} +code pre , pre code { padding: 0; } + + + +/* Elements */ +hr { + background: var(--text-color); + border: 0; + height: 1px; + margin: 4rem 0; +} + +img { max-width: 100%; } + +figure { + border: 1px solid var(--primary-color); + display: inline-block; + padding: 1rem; + width: auto; +} +figure img { margin: 0; } +figure figcaption { font-size: 80%; } + +ul, ol { margin: 2rem 0; padding: 0 0 0 4rem; } + +dl dd { padding-left: 2rem; } + +table { + border: 1px solid var(--primary-color); + border-collapse: collapse; + table-layout: fixed; + width: 100%; +} +table caption { margin: 2rem 0; } +table thead { text-align: center; } +table tbody { text-align: right; } +table tr { border-bottom: 1px solid var(--primary-color); } +table tbody tr:nth-child(even) { background: var(--tertiary-color); } +table th { background: var(--secondary-color); font-weight: bold; } +table th, table td { padding: 1rem; } +table th:not(last-of-type), table td:not(last-of-type) { border-right: 1px solid var(--primary-color); } + + + +/* Mobile Styling */ +@media screen and (max-width: 50rem) { + body { + font: var(--mobile-font-size) -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto, Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol" + } +} \ No newline at end of file diff --git a/src/server/public/js/new.js b/src/server/public/js/new.js new file mode 100644 index 000000000..beaa36da6 --- /dev/null +++ b/src/server/public/js/new.js @@ -0,0 +1,24 @@ +/* eslint-env browser */ +const elDrop = document.getElementById('dropzone'); +const submitBtn = document.getElementById('submitbutton'); +const fileInput = document.getElementById('files'); + +elDrop.addEventListener('dragover', (event) => { + event.preventDefault(); +}); + +elDrop.addEventListener('drop', async (event) => { + event.preventDefault(); + + submitBtn.disabled = true; + + fileInput.files = event.dataTransfer.files; + + elDrop.innerText = `✓ ${fileInput.files.length} files selected`; + elDrop.style.height = 'inherit'; + + submitBtn.disabled = false; +}); +elDrop.addEventListener('click', () => { + fileInput.click(); +}); diff --git a/src/server/resources/views/layouts/main.edge b/src/server/resources/views/layouts/main.edge new file mode 100644 index 000000000..b6cc8ca23 --- /dev/null +++ b/src/server/resources/views/layouts/main.edge @@ -0,0 +1,18 @@ + + + + + + + + ferdi-server + + {{ style('css/vanilla') }} + {{ style('css/main') }} + + + + @!section('content') + + + diff --git a/src/server/resources/views/others/import.edge b/src/server/resources/views/others/import.edge new file mode 100644 index 000000000..b890bbb2a --- /dev/null +++ b/src/server/resources/views/others/import.edge @@ -0,0 +1,19 @@ +@layout('layouts.main') + +@section('content') +

Import a Franz account

+

Please login using your Franz account. We will create a new Ferdi account with the same credentials.

+
+
+
+ +
+
+ + + + By importing your Franz account, you accept the Terms of service and Privacy + policy + +
+@endsection diff --git a/src/server/resources/views/others/index.edge b/src/server/resources/views/others/index.edge new file mode 100644 index 000000000..c594d3142 --- /dev/null +++ b/src/server/resources/views/others/index.edge @@ -0,0 +1,38 @@ +@layout('layouts.main') + +@section('content') + +

ferdi-server

+

You are accessing a custom Ferdi server.

+

+ To use this server in your Ferdi client, open Ferdi's settings and as the + server, enter +

+

+ Alternatively, you can manage your account in the account dashboard. +

+ +
+ + ferdi-server is a project by vantezzen. + + + +@endsection diff --git a/src/server/resources/views/others/new.edge b/src/server/resources/views/others/new.edge new file mode 100644 index 000000000..1b54558fc --- /dev/null +++ b/src/server/resources/views/others/new.edge @@ -0,0 +1,40 @@ +@layout('layouts.main') + +@section('content') +

Create a new recipe

+

Please create a recipe using the + official Franz guide, then publish it here.

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ *These images must be publicly availible and have CORS enabled in order to work.

+ +
+
+
+ Drop recipe files here
or click here to select files +

+ Drag and drop your recipe files into this area.
+ Please do not select the folder that contains the files but rather the files itself. +

+
+
+

+ + +
+ + +@endsection diff --git a/src/server/start.js b/src/server/start.js new file mode 100644 index 000000000..8a8711a78 --- /dev/null +++ b/src/server/start.js @@ -0,0 +1,40 @@ + +/* +|-------------------------------------------------------------------------- +| Http server +|-------------------------------------------------------------------------- +| +| This file bootstraps Adonisjs to start the HTTP server. You are free to +| customize the process of booting the http server. +| +| """ Loading ace commands """ +| At times you may want to load ace commands when starting the HTTP server. +| Same can be done by chaining `loadCommands()` method after +| +| """ Preloading files """ +| Also you can preload files by calling `preLoad('path/to/file')` method. +| Make sure to pass a relative path from the project root. +*/ +const path = require('path'); +const fs = require('fs-extra'); +// eslint-disable-next-line import/no-extraneous-dependencies +const { app } = require('electron'); + +process.env.ENV_PATH = path.join(__dirname, 'env.ini'); + +// Make sure local database exists +const dbPath = path.join(app.getPath('userData'), 'server.sqlite'); +if (!fs.existsSync(dbPath)) { + fs.copySync( + path.join(__dirname, 'database', 'template.sqlite'), + dbPath, + ); +} + +const { Ignitor } = require('@adonisjs/ignitor'); +const fold = require('@adonisjs/fold'); + +new Ignitor(fold) + .appRoot(__dirname) + .fireHttpServer() + .catch(console.error); // eslint-disable-line no-console diff --git a/src/server/start/app.js b/src/server/start/app.js new file mode 100644 index 000000000..a29ca6594 --- /dev/null +++ b/src/server/start/app.js @@ -0,0 +1,62 @@ + +/* +|-------------------------------------------------------------------------- +| Providers +|-------------------------------------------------------------------------- +| +| Providers are building blocks for your Adonis app. Anytime you install +| a new Adonis specific package, chances are you will register the +| provider here. +| +*/ +const providers = [ + '@adonisjs/framework/providers/AppProvider', + '@adonisjs/bodyparser/providers/BodyParserProvider', + '@adonisjs/cors/providers/CorsProvider', + '@adonisjs/lucid/providers/LucidProvider', + '@adonisjs/drive/providers/DriveProvider', + '@adonisjs/validator/providers/ValidatorProvider', + '@adonisjs/framework/providers/ViewProvider', + '@adonisjs/shield/providers/ShieldProvider', +]; + +/* +|-------------------------------------------------------------------------- +| Ace Providers +|-------------------------------------------------------------------------- +| +| Ace providers are required only when running ace commands. For example +| Providers for migrations, tests etc. +| +*/ +const aceProviders = [ + '@adonisjs/lucid/providers/MigrationsProvider', +]; + +/* +|-------------------------------------------------------------------------- +| Aliases +|-------------------------------------------------------------------------- +| +| Aliases are short unique names for IoC container bindings. You are free +| to create your own aliases. +| +| For example: +| { Route: 'Adonis/Src/Route' } +| +*/ +const aliases = {}; + +/* +|-------------------------------------------------------------------------- +| Commands +|-------------------------------------------------------------------------- +| +| Here you store ace commands for your package +| +*/ +const commands = []; + +module.exports = { + providers, aceProviders, aliases, commands, +}; diff --git a/src/server/start/kernel.js b/src/server/start/kernel.js new file mode 100644 index 000000000..54fe1f35d --- /dev/null +++ b/src/server/start/kernel.js @@ -0,0 +1,56 @@ + +/** @type {import('@adonisjs/framework/src/Server')} */ +const Server = use('Server'); + +/* +|-------------------------------------------------------------------------- +| Global Middleware +|-------------------------------------------------------------------------- +| +| Global middleware are executed on each http request only when the routes +| match. +| +*/ +const globalMiddleware = [ + 'Adonis/Middleware/BodyParser', + 'App/Middleware/ConvertEmptyStringsToNull', +]; + +/* +|-------------------------------------------------------------------------- +| Named Middleware +|-------------------------------------------------------------------------- +| +| Named middleware is key/value object to conditionally add middleware on +| specific routes or group of routes. +| +| // define +| { +| auth: 'Adonis/Middleware/Auth' +| } +| +| // use +| Route.get().middleware('auth') +| +*/ +const namedMiddleware = { +}; + +/* +|-------------------------------------------------------------------------- +| Server Middleware +|-------------------------------------------------------------------------- +| +| Server level middleware are executed even when route for a given URL is +| not registered. Features like `static assets` and `cors` needs better +| control over request lifecycle. +| +*/ +const serverMiddleware = [ + 'Adonis/Middleware/Static', +]; + +Server + .registerGlobal(globalMiddleware) + .registerNamed(namedMiddleware) + .use(serverMiddleware); diff --git a/src/server/start/routes.js b/src/server/start/routes.js new file mode 100644 index 000000000..f3896bfa3 --- /dev/null +++ b/src/server/start/routes.js @@ -0,0 +1,74 @@ + +/* +|-------------------------------------------------------------------------- +| Routes +|-------------------------------------------------------------------------- +| +*/ + +/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ +const Route = use('Route'); +const Env = use('Env'); + +// Health: Returning if all systems function correctly +Route.get('health', ({ + response, +}) => response.send({ + api: 'success', + db: 'success', +})); + +// API is grouped under '/v1/' route +Route.group(() => { + // User authentification + Route.post('auth/signup', 'UserController.signup'); + Route.post('auth/login', 'UserController.login'); + + // User info + Route.get('me', 'UserController.me'); + + // Service info + Route.post('service', 'ServiceController.create'); + Route.put('service/:id', 'ServiceController.edit'); + Route.delete('service/:id', 'ServiceController.delete'); + Route.get('me/services', 'ServiceController.list'); + Route.put('service/reorder', 'ServiceController.reorder'); + Route.get('recipe', 'ServiceController.list'); + Route.post('recipes/update', 'ServiceController.update'); + + // Recipe store + Route.get('recipes', 'RecipeController.list'); + Route.get('recipes/download/:recipe', 'RecipeController.download'); + Route.get('recipes/search', 'RecipeController.search'); + Route.get('recipes/popular', 'StaticController.popularRecipes'); + Route.get('recipes/update', 'StaticController.emptyArray'); + + // Workspaces + Route.put('workspace/:id', 'WorkspaceController.edit'); + Route.delete('workspace/:id', 'WorkspaceController.delete'); + Route.post('workspace', 'WorkspaceController.create'); + Route.get('workspace', 'WorkspaceController.list'); + + // Static responses + Route.get('features', 'StaticController.features'); + Route.get('services', 'StaticController.emptyArray'); + Route.get('news', 'StaticController.emptyArray'); + Route.get('payment/plans', 'StaticController.plans'); + Route.get('announcements/:version', 'StaticController.announcement'); +}).prefix('v1'); + +// Recipe creation +Route.post('new', 'RecipeController.create'); +Route.get('new', ({ response, view }) => { + if (Env.get('IS_CREATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq + return response.send('This server doesn\'t allow the creation of new recipes.\n\nIf you are the server owner, please set IS_CREATION_ENABLED to true to enable recipe creation.'); + } + return view.render('others.new'); +}); + +// Franz account import +Route.post('import', 'UserController.import'); +Route.get('import', ({ view }) => view.render('others.import')); + +// Index +Route.get('/', ({ view }) => view.render('others.index')); -- cgit v1.2.3-70-g09d2 From 37c2448778e6595dc09291e46c4ba7dca89eb20d Mon Sep 17 00:00:00 2001 From: vantezzen Date: Mon, 14 Oct 2019 17:42:24 +0200 Subject: Develop internal server --- src/server/README.md | 35 +------- .../app/Controllers/Http/DashboardController.js | 38 --------- .../app/Controllers/Http/RecipeController.js | 91 +-------------------- src/server/app/Exceptions/Handler.js | 2 - src/server/app/Models/User.js | 2 +- src/server/database/ferdi.sqlite | Bin 36864 -> 0 bytes src/server/logo.png | Bin 340668 -> 298671 bytes src/server/public/js/new.js | 24 ------ src/server/resources/views/import.edge | 15 ++++ src/server/resources/views/index.edge | 32 ++++++++ src/server/resources/views/others/import.edge | 19 ----- src/server/resources/views/others/index.edge | 38 --------- src/server/resources/views/others/new.edge | 40 --------- src/server/start/routes.js | 13 +-- 14 files changed, 54 insertions(+), 295 deletions(-) delete mode 100644 src/server/app/Controllers/Http/DashboardController.js delete mode 100644 src/server/database/ferdi.sqlite delete mode 100644 src/server/public/js/new.js create mode 100644 src/server/resources/views/import.edge create mode 100644 src/server/resources/views/index.edge delete mode 100644 src/server/resources/views/others/import.edge delete mode 100644 src/server/resources/views/others/index.edge delete mode 100644 src/server/resources/views/others/new.edge (limited to 'src/server') diff --git a/src/server/README.md b/src/server/README.md index 833d9643e..0074f2314 100644 --- a/src/server/README.md +++ b/src/server/README.md @@ -5,23 +5,8 @@ # ferdi-internal-server Internal Ferdi Server used for storing settings without logging into an external server. -npm i @adonisjs/ace @adonisjs/auth @adonisjs/bodyparser @adonisjs/cors @adonisjs/drive @adonisjs/fold @adonisjs/framework @adonisjs/ignitor @adonisjs/lucid @adonisjs/session @adonisjs/shield @adonisjs/validator atob btoa fs-extra node-fetch sqlite3 uuid targz - -### Manual setup -1. Clone this repository -2. Install the [AdonisJS CLI](https://adonisjs.com/) -3. Run the database migrations with - ```js - adonis migration:run - ``` -4. Start the server with - ```js - adonis serve --dev - ``` - ## Configuration -franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's settings, ferdi-server has the following custom settings: -- `IS_CREATION_ENABLED` (`true` or `false`, default: `true`): Whether to enable the [creation of custom recipes](#creating-and-using-custom-recipes) +franz-server's configuration is saved inside the `env.ini` file. Besides AdonisJS's settings, ferdi-internal-server has the following custom settings: - `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-server can: - Show the full Franz recipe library instead of only custom recipes - Import Franz accounts @@ -30,21 +15,3 @@ franz-server's configuration is saved inside the `.env` file. Besides AdonisJS's ferdi-server allows you to import your full Franz account, including all its settings. To import your Franz account, open `http://localhost:45569/import` in your browser and login using your Franz account details. ferdi-server will create a new user with the same credentials and copy your Franz settings, services and workspaces. - -## Creating and using custom recipes -ferdi-server allows to extends the Franz recipe catalogue with custom Ferdi recipes. - -For documentation on how to create a recipe, please visit [the official guide by Franz](https://github.com/meetfranz/plugins/blob/master/docs/integration.md). - -To add your recipe to ferdi-server, open `http://localhost:45569/new` in your browser. You can now define the following settings: -- `Author`: Author who created the recipe -- `Name`: Name for your new service. Can contain spaces and unicode characters -- `Service ID`: Unique ID for this recipe. Does not contain spaces or special characters (e.g. `google-drive`) -- `Link to PNG/SVG image`: Direct link to a 1024x1024 PNG image and SVG that is used as a logo inside the store. Please use jsDelivr when using a file uploaded to GitHub as raw.githubusercontent files won't load -- `Recipe files`: Recipe files that you created using the [Franz recipe creation guide](https://github.com/meetfranz/plugins/blob/master/docs/integration.md). Please do *not* package your files beforehand - upload the raw files (you can drag and drop multiple files). ferdi-server will automatically package and store the recipe in the right format. Please also do not drag and drop or select the whole folder, select the individual files. - -### Listing custom recipes -Inside Ferdi, searching for `ferdi:custom` will list all your custom recipes. - -## License -ferdi-server is licensed under the MIT License diff --git a/src/server/app/Controllers/Http/DashboardController.js b/src/server/app/Controllers/Http/DashboardController.js deleted file mode 100644 index 69af16227..000000000 --- a/src/server/app/Controllers/Http/DashboardController.js +++ /dev/null @@ -1,38 +0,0 @@ -class DashboardController { - async data({ - auth, - view, - }) { - const general = auth.user; - const services = (await auth.user.services().fetch()).toJSON(); - const workspaces = (await auth.user.workspaces().fetch()).toJSON(); - - return view.render('dashboard.data', { - username: general.username, - mail: general.email, - created: general.created_at, - updated: general.updated_at, - services, - workspaces, - }); - } - - logout({ - auth, - response, - }) { - auth.authenticator('session').logout(); - return response.redirect('/user/login'); - } - - delete({ - auth, - response, - }) { - auth.user.delete(); - auth.authenticator('session').logout(); - return response.redirect('/user/login'); - } -} - -module.exports = DashboardController; diff --git a/src/server/app/Controllers/Http/RecipeController.js b/src/server/app/Controllers/Http/RecipeController.js index 5ed21122a..c887ea00e 100644 --- a/src/server/app/Controllers/Http/RecipeController.js +++ b/src/server/app/Controllers/Http/RecipeController.js @@ -8,29 +8,15 @@ const { const Env = use('Env'); const fetch = require('node-fetch'); -const targz = require('targz'); const path = require('path'); const fs = require('fs-extra'); -const compress = (src, dest) => new Promise((resolve, reject) => { - targz.compress({ - src, - dest, - }, (err) => { - if (err) { - reject(err); - } else { - resolve(dest); - } - }); -}); - class RecipeController { // List official and custom recipes async list({ response, }) { - const officialRecipes = JSON.parse(await (await fetch('https://api.franzinfra.com/v1/recipes')).text()); + const officialRecipes = JSON.parse(await (await fetch('https://api.getferdi.com/v1/recipes')).text()); const customRecipesArray = (await Recipe.all()).rows; const customRecipes = customRecipesArray.map(recipe => ({ id: recipe.recipeId, @@ -46,77 +32,6 @@ class RecipeController { return response.send(recipes); } - // Create a new recipe using the new.html page - async create({ - request, - response, - }) { - // Check if recipe creation is enabled - if (Env.get('IS_CREATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq - return response.send('This server doesn\'t allow the creation of new recipes.'); - } - - // Validate user input - const validation = await validateAll(request.all(), { - name: 'required|string', - id: 'required|unique:recipes,recipeId', - author: 'required|accepted', - png: 'required|url', - svg: 'required|url', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - const data = request.all(); - - if (!data.id) { - return response.send('Please provide an ID'); - } - - // Check for invalid characters - if (/\.{1,}/.test(data.id) || /\/{1,}/.test(data.id)) { - return response.send('Invalid recipe name. Your recipe name may not contain "." or "/"'); - } - - // Clear temporary recipe folder - await fs.emptyDir(Helpers.tmpPath('recipe')); - - // Move uploaded files to temporary path - const files = request.file('files'); - await files.moveAll(Helpers.tmpPath('recipe')); - - // Compress files to .tar.gz file - const source = Helpers.tmpPath('recipe'); - const destination = path.join(Helpers.appRoot(), `/recipes/${data.id}.tar.gz`); - - compress( - source, - destination, - ); - - // Create recipe in db - await Recipe.create({ - name: data.name, - recipeId: data.id, - data: JSON.stringify({ - author: data.author, - featured: false, - version: '1.0.0', - icons: { - png: data.png, - svg: data.svg, - }, - }), - }); - - return response.send('Created new recipe'); - } - // Search official and custom recipes async search({ request, @@ -149,7 +64,7 @@ class RecipeController { } else { let remoteResults = []; if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq - remoteResults = JSON.parse(await (await fetch(`https://api.franzinfra.com/v1/recipes/search?needle=${encodeURIComponent(needle)}`)).text()); + remoteResults = JSON.parse(await (await fetch(`https://api.getferdi.com/v1/recipes/search?needle=${encodeURIComponent(needle)}`)).text()); } const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON(); const localResults = localResultsArray.map(recipe => ({ @@ -195,7 +110,7 @@ class RecipeController { if (await Drive.exists(`${service}.tar.gz`)) { return response.send(await Drive.get(`${service}.tar.gz`)); } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq - return response.redirect(`https://api.franzinfra.com/v1/recipes/download/${service}`); + return response.redirect(`https://api.getferdi.com/v1/recipes/download/${service}`); } return response.status(400).send({ message: 'Recipe not found', diff --git a/src/server/app/Exceptions/Handler.js b/src/server/app/Exceptions/Handler.js index cb9e10bbe..e8d2d2ee2 100644 --- a/src/server/app/Exceptions/Handler.js +++ b/src/server/app/Exceptions/Handler.js @@ -22,8 +22,6 @@ class ExceptionHandler extends BaseExceptionHandler { async handle(error, { response }) { if (error.name === 'ValidationException') { return response.status(400).send('Invalid arguments'); - } if (error.name === 'InvalidSessionException') { - return response.status(401).redirect('/user/login'); } return response.status(error.status).send(error.message); diff --git a/src/server/app/Models/User.js b/src/server/app/Models/User.js index 9783cbe45..907710d8d 100644 --- a/src/server/app/Models/User.js +++ b/src/server/app/Models/User.js @@ -1,4 +1,4 @@ - +// File is required by AdonisJS but not used by the server /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ const Model = use('Model'); diff --git a/src/server/database/ferdi.sqlite b/src/server/database/ferdi.sqlite deleted file mode 100644 index db5425ee6..000000000 Binary files a/src/server/database/ferdi.sqlite and /dev/null differ diff --git a/src/server/logo.png b/src/server/logo.png index 587e0b86e..4145a077a 100644 Binary files a/src/server/logo.png and b/src/server/logo.png differ diff --git a/src/server/public/js/new.js b/src/server/public/js/new.js deleted file mode 100644 index beaa36da6..000000000 --- a/src/server/public/js/new.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-env browser */ -const elDrop = document.getElementById('dropzone'); -const submitBtn = document.getElementById('submitbutton'); -const fileInput = document.getElementById('files'); - -elDrop.addEventListener('dragover', (event) => { - event.preventDefault(); -}); - -elDrop.addEventListener('drop', async (event) => { - event.preventDefault(); - - submitBtn.disabled = true; - - fileInput.files = event.dataTransfer.files; - - elDrop.innerText = `✓ ${fileInput.files.length} files selected`; - elDrop.style.height = 'inherit'; - - submitBtn.disabled = false; -}); -elDrop.addEventListener('click', () => { - fileInput.click(); -}); diff --git a/src/server/resources/views/import.edge b/src/server/resources/views/import.edge new file mode 100644 index 000000000..f7b52b179 --- /dev/null +++ b/src/server/resources/views/import.edge @@ -0,0 +1,15 @@ +@layout('layouts.main') + +@section('content') +

Import a Franz account

+

Please login using your Franz account. We will import your services and workspaces.

+
+
+
+ +
+
+ + +
+@endsection diff --git a/src/server/resources/views/index.edge b/src/server/resources/views/index.edge new file mode 100644 index 000000000..3e0198a09 --- /dev/null +++ b/src/server/resources/views/index.edge @@ -0,0 +1,32 @@ +@layout('layouts.main') + +@section('content') + +

Internal Ferdi Server

+

You are accessing the local server instance of your Ferdi application. This server is used to enable Ferdi's "Use without an Account" feature.

+

+ To use this server in your Ferdi client, open Ferdi's settings and as the + server, enter +

+

+ Alternatively, you can import your Franz account. +

+ + +@endsection diff --git a/src/server/resources/views/others/import.edge b/src/server/resources/views/others/import.edge deleted file mode 100644 index b890bbb2a..000000000 --- a/src/server/resources/views/others/import.edge +++ /dev/null @@ -1,19 +0,0 @@ -@layout('layouts.main') - -@section('content') -

Import a Franz account

-

Please login using your Franz account. We will create a new Ferdi account with the same credentials.

-
-
-
- -
-
- - - - By importing your Franz account, you accept the Terms of service and Privacy - policy - -
-@endsection diff --git a/src/server/resources/views/others/index.edge b/src/server/resources/views/others/index.edge deleted file mode 100644 index c594d3142..000000000 --- a/src/server/resources/views/others/index.edge +++ /dev/null @@ -1,38 +0,0 @@ -@layout('layouts.main') - -@section('content') - -

ferdi-server

-

You are accessing a custom Ferdi server.

-

- To use this server in your Ferdi client, open Ferdi's settings and as the - server, enter -

-

- Alternatively, you can manage your account in the account dashboard. -

- -
- - ferdi-server is a project by vantezzen. - - - -@endsection diff --git a/src/server/resources/views/others/new.edge b/src/server/resources/views/others/new.edge deleted file mode 100644 index 1b54558fc..000000000 --- a/src/server/resources/views/others/new.edge +++ /dev/null @@ -1,40 +0,0 @@ -@layout('layouts.main') - -@section('content') -

Create a new recipe

-

Please create a recipe using the - official Franz guide, then publish it here.

-
-
-
- -
-
- -
-
- -
-
- -
-
- *These images must be publicly availible and have CORS enabled in order to work.

- -
-
-
- Drop recipe files here
or click here to select files -

- Drag and drop your recipe files into this area.
- Please do not select the folder that contains the files but rather the files itself. -

-
-
-

- - -
- - -@endsection diff --git a/src/server/start/routes.js b/src/server/start/routes.js index f3896bfa3..ec2e79a7c 100644 --- a/src/server/start/routes.js +++ b/src/server/start/routes.js @@ -57,18 +57,9 @@ Route.group(() => { Route.get('announcements/:version', 'StaticController.announcement'); }).prefix('v1'); -// Recipe creation -Route.post('new', 'RecipeController.create'); -Route.get('new', ({ response, view }) => { - if (Env.get('IS_CREATION_ENABLED') == 'false') { // eslint-disable-line eqeqeq - return response.send('This server doesn\'t allow the creation of new recipes.\n\nIf you are the server owner, please set IS_CREATION_ENABLED to true to enable recipe creation.'); - } - return view.render('others.new'); -}); - // Franz account import Route.post('import', 'UserController.import'); -Route.get('import', ({ view }) => view.render('others.import')); +Route.get('import', ({ view }) => view.render('import')); // Index -Route.get('/', ({ view }) => view.render('others.index')); +Route.get('/', ({ view }) => view.render('index')); -- cgit v1.2.3-70-g09d2 From 5730de199a85ef735c3df7a37a755be0cadfb9f9 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Mon, 14 Oct 2019 18:04:25 +0200 Subject: Only allow Ferdi clients to connect --- src/server/start/routes.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src/server') diff --git a/src/server/start/routes.js b/src/server/start/routes.js index ec2e79a7c..d2ddfbd38 100644 --- a/src/server/start/routes.js +++ b/src/server/start/routes.js @@ -10,13 +10,22 @@ const Route = use('Route'); const Env = use('Env'); +const OnlyAllowFerdi = async ({ request, response }, next) => { + const user = request.header('User-Agent'); + if (!/Ferdi\/\d(\.\d){2}/g.test(user)) { + return response.status(403).redirect('/'); + } + + await next() +}; + // Health: Returning if all systems function correctly Route.get('health', ({ response, }) => response.send({ api: 'success', db: 'success', -})); +})).middleware(OnlyAllowFerdi); // API is grouped under '/v1/' route Route.group(() => { @@ -55,7 +64,7 @@ Route.group(() => { Route.get('news', 'StaticController.emptyArray'); Route.get('payment/plans', 'StaticController.plans'); Route.get('announcements/:version', 'StaticController.announcement'); -}).prefix('v1'); +}).prefix('v1').middleware(OnlyAllowFerdi); // Franz account import Route.post('import', 'UserController.import'); -- cgit v1.2.3-70-g09d2 From 0c06abeb282563b03a4b0f61f42b202e26d53e13 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Mon, 14 Oct 2019 18:04:38 +0200 Subject: Fix AdonisJs errors --- src/server/app/Controllers/Http/UserController.js | 4 +- src/server/package.json | 49 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/server/package.json (limited to 'src/server') diff --git a/src/server/app/Controllers/Http/UserController.js b/src/server/app/Controllers/Http/UserController.js index 758250ec9..ee6a82702 100644 --- a/src/server/app/Controllers/Http/UserController.js +++ b/src/server/app/Controllers/Http/UserController.js @@ -38,7 +38,7 @@ class UserController { // Validate user input const validation = await validateAll(request.all(), { firstname: 'required', - email: 'required|email|unique:users,email', + email: 'required|email', password: 'required', }); if (validation.fails()) { @@ -100,7 +100,7 @@ class UserController { }) { // Validate user input const validation = await validateAll(request.all(), { - email: 'required|email|unique:users,email', + email: 'required|email', password: 'required', }); if (validation.fails()) { diff --git a/src/server/package.json b/src/server/package.json new file mode 100644 index 000000000..60dd58e93 --- /dev/null +++ b/src/server/package.json @@ -0,0 +1,49 @@ +{ + "name": "ferdi-internal-server", + "version": "1.0.0", + "adonis-version": "4.1.0", + "description": "Internal server used by the Ferdi application.", + "main": "index.js", + "scripts": { + "start": "node server.js", + "test": "node ace test", + "lint": "eslint --fix ./" + }, + "keywords": [ + ], + "author": "", + "license": "MIT License", + "private": true, + "dependencies": { + "@adonisjs/ace": "^5.0.8", + "@adonisjs/auth": "^3.0.7", + "@adonisjs/bodyparser": "^2.0.5", + "@adonisjs/cors": "^1.0.7", + "@adonisjs/drive": "^1.0.4", + "@adonisjs/fold": "^4.0.9", + "@adonisjs/framework": "^5.0.9", + "@adonisjs/ignitor": "^2.0.8", + "@adonisjs/lucid": "^6.1.3", + "@adonisjs/session": "^1.0.29", + "@adonisjs/shield": "^1.0.8", + "@adonisjs/validator": "^5.0.6", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fs-extra": "^8.1.0", + "node-fetch": "^2.6.0", + "sqlite3": "^4.1.0", + "uuid": "^3.3.3" + }, + "devDependencies": { + "eslint": "^6.3.0", + "eslint-config-airbnb": "^18.0.1", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-react": "^7.14.3", + "eslint-plugin-react-hooks": "^1.7.0" + }, + "autoload": { + "App": "./app" + } +} -- cgit v1.2.3-70-g09d2 From 3f1bea40281af57df9aae3888634123addefe3dd Mon Sep 17 00:00:00 2001 From: vantezzen Date: Mon, 14 Oct 2019 18:11:09 +0200 Subject: Fix lint --- src/server/app/Controllers/Http/RecipeController.js | 3 --- src/server/resources/views/layouts/main.edge | 2 +- src/server/start/routes.js | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) (limited to 'src/server') diff --git a/src/server/app/Controllers/Http/RecipeController.js b/src/server/app/Controllers/Http/RecipeController.js index c887ea00e..71ac12c0f 100644 --- a/src/server/app/Controllers/Http/RecipeController.js +++ b/src/server/app/Controllers/Http/RecipeController.js @@ -1,6 +1,5 @@ const Recipe = use('App/Models/Recipe'); -const Helpers = use('Helpers'); const Drive = use('Drive'); const { validateAll, @@ -8,8 +7,6 @@ const { const Env = use('Env'); const fetch = require('node-fetch'); -const path = require('path'); -const fs = require('fs-extra'); class RecipeController { // List official and custom recipes diff --git a/src/server/resources/views/layouts/main.edge b/src/server/resources/views/layouts/main.edge index b6cc8ca23..77af30327 100644 --- a/src/server/resources/views/layouts/main.edge +++ b/src/server/resources/views/layouts/main.edge @@ -5,7 +5,7 @@ - ferdi-server + ferdi-internal-server {{ style('css/vanilla') }} {{ style('css/main') }} diff --git a/src/server/start/routes.js b/src/server/start/routes.js index d2ddfbd38..333a5ba06 100644 --- a/src/server/start/routes.js +++ b/src/server/start/routes.js @@ -8,7 +8,6 @@ /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ const Route = use('Route'); -const Env = use('Env'); const OnlyAllowFerdi = async ({ request, response }, next) => { const user = request.header('User-Agent'); @@ -16,7 +15,8 @@ const OnlyAllowFerdi = async ({ request, response }, next) => { return response.status(403).redirect('/'); } - await next() + await next(); + return true; }; // Health: Returning if all systems function correctly -- cgit v1.2.3-70-g09d2 From af506f40edb1c9c339cc86baf40baccf2dc6da62 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Fri, 18 Oct 2019 20:53:41 +0200 Subject: Develop local server feature --- src/api/apiBase.js | 6 +++++ src/config.js | 2 ++ src/electron/ipc-api/index.js | 2 ++ src/electron/ipc-api/localServer.js | 52 +++++++++++++++++++++++++++++++++++++ src/helpers/serverless-helpers.js | 4 ++- src/i18n/locales/zh-HANT.json | 6 +++++ src/index.js | 3 --- src/server/config/database.js | 6 +---- src/server/start.js | 31 +++++++++++----------- src/stores/RequestStore.js | 9 +++++++ src/stores/SettingsStore.js | 14 +++++++++- 11 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 src/electron/ipc-api/localServer.js (limited to 'src/server') diff --git a/src/api/apiBase.js b/src/api/apiBase.js index e8d571171..561b025f0 100644 --- a/src/api/apiBase.js +++ b/src/api/apiBase.js @@ -4,6 +4,9 @@ import { API_VERSION, } from '../environment'; +import { + LOCAL_SERVER, +} from '../config'; const apiBase = () => { let url; @@ -21,6 +24,9 @@ const apiBase = () => { // on some routes. This would result in Ferdi deleting its current authToken as it thinks it // has gone invalid. url = 'https://1.1.1.1'; + } else if (window.ferdi.stores.settings.all.app.server === LOCAL_SERVER) { + // Use URL for local server + url = `http://127.0.0.1:${window.ferdi.stores.requests.localServerPort}`; } else { // Load URL from store url = window.ferdi.stores.settings.all.app.server; diff --git a/src/config.js b/src/config.js index 0673e994a..e30a6d4b2 100644 --- a/src/config.js +++ b/src/config.js @@ -111,6 +111,8 @@ export const FILE_SYSTEM_SETTINGS_TYPES = [ 'proxy', ]; +export const LOCAL_SERVER = 'You are using Ferdi without a server'; + export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config'); // Replacing app.asar is not beautiful but unforunately necessary diff --git a/src/electron/ipc-api/index.js b/src/electron/ipc-api/index.js index 3b7f31e4b..dcdef6b32 100644 --- a/src/electron/ipc-api/index.js +++ b/src/electron/ipc-api/index.js @@ -3,6 +3,7 @@ import settings from './settings'; import appIndicator from './appIndicator'; import download from './download'; import processManager from './processManager'; +import localServer from './localServer'; export default (params) => { settings(params); @@ -10,4 +11,5 @@ export default (params) => { appIndicator(params); download(params); processManager(params); + localServer(params); }; diff --git a/src/electron/ipc-api/localServer.js b/src/electron/ipc-api/localServer.js new file mode 100644 index 000000000..2f8f1020a --- /dev/null +++ b/src/electron/ipc-api/localServer.js @@ -0,0 +1,52 @@ +import { ipcMain, app } from 'electron'; +import path from 'path'; +import net from 'net'; +import startServer from '../../server/start'; + +const DEFAULT_PORT = 45569; + +const portInUse = function (port) { + return new Promise((resolve) => { + const server = net.createServer((socket) => { + socket.write('Echo server\r\n'); + socket.pipe(socket); + }); + + server.listen(port, '127.0.0.1'); + server.on('error', () => { + resolve(true); + }); + server.on('listening', () => { + server.close(); + resolve(false); + }); + }); +}; + +let localServerStarted = false; + +export default (params) => { + ipcMain.on('startLocalServer', () => { + if (!localServerStarted) { + // Find next unused port for server + let port = DEFAULT_PORT; + (async () => { + // eslint-disable-next-line no-await-in-loop + while (await portInUse(port) && port < DEFAULT_PORT + 10) { + port += 1; + } + console.log('Starting local server on port', port); + + startServer( + path.join(app.getPath('userData'), 'server.sqlite'), + port, + ); + + params.mainWindow.webContents.send('localServerPort', { + port, + }); + })(); + localServerStarted = true; + } + }); +}; diff --git a/src/helpers/serverless-helpers.js b/src/helpers/serverless-helpers.js index 741bce7f9..01549e038 100644 --- a/src/helpers/serverless-helpers.js +++ b/src/helpers/serverless-helpers.js @@ -1,9 +1,11 @@ +import { LOCAL_SERVER } from '../config'; + export default function useLocalServer(actions) { // Use local server for user actions.settings.update({ type: 'app', data: { - server: 'http://localhost:45569', + server: LOCAL_SERVER, }, }); diff --git a/src/i18n/locales/zh-HANT.json b/src/i18n/locales/zh-HANT.json index 5784c1ab6..678554c05 100644 --- a/src/i18n/locales/zh-HANT.json +++ b/src/i18n/locales/zh-HANT.json @@ -11,6 +11,7 @@ "feature.delayApp.upgrade.actionShort": "Upgrade account", "feature.quickSwitch.info": "Select a service with TAB, ↑ and ↓. Open a service with ENTER.", "feature.quickSwitch.search": "Search...", + "feature.quickSwitch.title": "QuickSwitch", "feature.serviceLimit.limitReached": "You have added {amount} out of {limit} services that are included in your plan. Please upgrade your account to add more services.", "feature.shareFranz.action.email": "Send as email", "feature.shareFranz.action.facebook": "Share on Facebook", @@ -213,11 +214,13 @@ "settings.account.upgradeToPro.label": "Upgrade to Ferdi Professional", "settings.account.userInfoRequestFailed": "無法載入帳戶資訊", "settings.account.yourLicense": "Your Ferdi License", + "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: #7367f0)", "settings.app.buttonClearAllCache": "Clear cache", "settings.app.buttonInstallUpdate": "重新啟動並且更新", "settings.app.buttonSearchForUpdate": "Check for updates", "settings.app.cacheInfo": "Ferdi cache is currently using {size} of disk space.", "settings.app.currentVersion": "當前版本:", + "settings.app.form.accentColor": "Accent color", "settings.app.form.autoLaunchInBackground": "背景啟動", "settings.app.form.autoLaunchOnStart": "開機時啟動", "settings.app.form.beta": "包含開發中版本", @@ -244,6 +247,7 @@ "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled", "settings.app.form.showServiceNavigationBar": "Always show service navigation bar", "settings.app.form.todoServer": "Todo Server", + "settings.app.form.universalDarkMode": "Enable universal Dark Mode", "settings.app.headline": "Settings", "settings.app.headlineAdvanced": "Advanced", "settings.app.headlineAppearance": "Appearance", @@ -263,6 +267,7 @@ "settings.app.subheadlineCache": "Cache", "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature. (default: https://app.franztodos.com)", "settings.app.translationHelp": "Help us to translate Ferdi into your language.", + "settings.app.universalDarkModeInfo": "Universal Dark Mode tries to dynamically generate dark mode styles for services that are otherwise not currently supported.", "settings.app.updateStatusAvailable": "有可用更新,下載中...", "settings.app.updateStatusSearching": "檢查更新中...", "settings.app.updateStatusUpToDate": "已經是最新版本了", @@ -315,6 +320,7 @@ "settings.service.form.indirectMessages": "針對全部訊息顯示通知", "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", "settings.service.form.name": "名子", + "settings.service.form.openDarkmodeCss": "Open darkmode.css", "settings.service.form.proxy.headline": "HTTP/HTTPS Proxy Settings", "settings.service.form.proxy.host": "Proxy Host/IP", "settings.service.form.proxy.info": "Proxy settings will not synced with the Ferdi servers.", diff --git a/src/index.js b/src/index.js index 7a0e89285..4d7215d5e 100644 --- a/src/index.js +++ b/src/index.js @@ -34,9 +34,6 @@ import { isPositionValid } from './electron/windowUtils'; import { appId } from './package.json'; // eslint-disable-line import/no-unresolved import './electron/exception'; -// Start internal server -import './server/start'; - import { DEFAULT_APP_SETTINGS, DEFAULT_WINDOW_OPTIONS, diff --git a/src/server/config/database.js b/src/server/config/database.js index 86f18dac5..a413f7050 100644 --- a/src/server/config/database.js +++ b/src/server/config/database.js @@ -2,11 +2,7 @@ /** @type {import('@adonisjs/framework/src/Env')} */ const Env = use('Env'); -// eslint-disable-next-line import/no-extraneous-dependencies -const { app } = require('electron'); -const path = require('path'); - -const dbPath = path.join(app.getPath('userData'), 'server.sqlite'); +const dbPath = process.env.DB_PATH; module.exports = { /* diff --git a/src/server/start.js b/src/server/start.js index 8a8711a78..34b2cb5fa 100644 --- a/src/server/start.js +++ b/src/server/start.js @@ -17,24 +17,25 @@ */ const path = require('path'); const fs = require('fs-extra'); -// eslint-disable-next-line import/no-extraneous-dependencies -const { app } = require('electron'); process.env.ENV_PATH = path.join(__dirname, 'env.ini'); -// Make sure local database exists -const dbPath = path.join(app.getPath('userData'), 'server.sqlite'); -if (!fs.existsSync(dbPath)) { - fs.copySync( - path.join(__dirname, 'database', 'template.sqlite'), - dbPath, - ); -} - const { Ignitor } = require('@adonisjs/ignitor'); const fold = require('@adonisjs/fold'); -new Ignitor(fold) - .appRoot(__dirname) - .fireHttpServer() - .catch(console.error); // eslint-disable-line no-console +module.exports = (dbPath, port) => { + if (!fs.existsSync(dbPath)) { + fs.copySync( + path.join(__dirname, 'database', 'template.sqlite'), + dbPath, + ); + } + + process.env.DB_PATH = dbPath; + process.env.PORT = port; + + new Ignitor(fold) + .appRoot(__dirname) + .fireHttpServer() + .catch(console.error); // eslint-disable-line no-console +}; diff --git a/src/stores/RequestStore.js b/src/stores/RequestStore.js index 2587d4eef..a92f4c685 100644 --- a/src/stores/RequestStore.js +++ b/src/stores/RequestStore.js @@ -1,3 +1,4 @@ +import { ipcRenderer } from 'electron'; import { action, computed, observable } from 'mobx'; import ms from 'ms'; @@ -12,6 +13,8 @@ export default class RequestStore extends Store { @observable showRequiredRequestsError = false; + @observable localServerPort = 45569; + retries = 0; retryDelay = ms('2s'); @@ -29,6 +32,12 @@ export default class RequestStore extends Store { setup() { this.userInfoRequest = this.stores.user.getUserInfoRequest; this.servicesRequest = this.stores.services.allServicesRequest; + + ipcRenderer.on('localServerPort', (event, data) => { + if (data.port) { + this.localServerPort = data.port; + } + }); } @computed get areRequiredRequestsSuccessful() { diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js index 8c4cd47eb..df0fc77e9 100644 --- a/src/stores/SettingsStore.js +++ b/src/stores/SettingsStore.js @@ -9,7 +9,7 @@ import Request from './lib/Request'; import { getLocale } from '../helpers/i18n-helpers'; import { API } from '../environment'; -import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config'; +import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES, LOCAL_SERVER } from '../config'; import { SPELLCHECKER_LOCALES } from '../i18n/languages'; const debug = require('debug')('Ferdi:SettingsStore'); @@ -52,6 +52,18 @@ export default class SettingsStore extends Store { ), ); + reaction( + () => this.all.app.server, + (server) => { + if (server === LOCAL_SERVER) { + ipcRenderer.send('startLocalServer'); + } + }, + { + fireImmediately: true, + }, + ); + reaction( () => this.all.app.locked, () => { -- cgit v1.2.3-70-g09d2 From dd1374a9d91780935ff360002b14560228cbae4b Mon Sep 17 00:00:00 2001 From: vantezzen Date: Fri, 18 Oct 2019 21:18:20 +0200 Subject: Allow import data from different servers --- src/server/app/Controllers/Http/UserController.js | 18 ++++++++---------- src/server/resources/views/import.edge | 3 +++ 2 files changed, 11 insertions(+), 10 deletions(-) (limited to 'src/server') diff --git a/src/server/app/Controllers/Http/UserController.js b/src/server/app/Controllers/Http/UserController.js index ee6a82702..be7ebe5fe 100644 --- a/src/server/app/Controllers/Http/UserController.js +++ b/src/server/app/Controllers/Http/UserController.js @@ -10,8 +10,8 @@ const fetch = require('node-fetch'); const uuid = require('uuid/v4'); const crypto = require('crypto'); -const franzRequest = (route, method, auth) => new Promise((resolve, reject) => { - const base = 'https://api.franzinfra.com/v1/'; +const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => { + const base = url + '/v1/'; const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; try { @@ -102,6 +102,7 @@ class UserController { const validation = await validateAll(request.all(), { email: 'required|email', password: 'required', + server: 'required', }); if (validation.fails()) { let errorMessage = 'There was an error while trying to import your account:\n'; @@ -120,15 +121,12 @@ class UserController { const { email, password, + server, } = request.all(); const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); - if (Env.get('CONNECT_WITH_FRANZ') == 'false') { // eslint-disable-line eqeqeq - return response.send('Your account has been created but due to this server\'s configuration, we could not import your Franz account data.\n\nIf you are the server owner, please set CONNECT_WITH_FRANZ to true to enable account imports.'); - } - - const base = 'https://api.franzinfra.com/v1/'; + const base = server + '/v1/'; const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; // Try to get an authentication token @@ -162,7 +160,7 @@ class UserController { // Get user information let userInf = false; try { - userInf = await franzRequest('me', 'GET', token); + userInf = await apiRequest(server, 'me', 'GET', token); } catch (e) { const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`; return response.status(401).send(errorMessage); @@ -176,7 +174,7 @@ class UserController { // Import services try { - const services = await franzRequest('me/services', 'GET', token); + const services = await apiRequest(server, 'me/services', 'GET', token); for (const service of services) { // Get new, unused uuid @@ -201,7 +199,7 @@ class UserController { // Import workspaces try { - const workspaces = await franzRequest('workspace', 'GET', token); + const workspaces = await apiRequest(server, 'workspace', 'GET', token); for (const workspace of workspaces) { let workspaceId; diff --git a/src/server/resources/views/import.edge b/src/server/resources/views/import.edge index f7b52b179..561021a0c 100644 --- a/src/server/resources/views/import.edge +++ b/src/server/resources/views/import.edge @@ -10,6 +10,9 @@

+
+
+ @endsection -- cgit v1.2.3-70-g09d2 From b95bf31e572f0a936b1a138c5c5278ea050ba8b5 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Fri, 18 Oct 2019 21:20:45 +0200 Subject: Fix lint --- src/server/app/Controllers/Http/UserController.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/server') diff --git a/src/server/app/Controllers/Http/UserController.js b/src/server/app/Controllers/Http/UserController.js index be7ebe5fe..07e118afd 100644 --- a/src/server/app/Controllers/Http/UserController.js +++ b/src/server/app/Controllers/Http/UserController.js @@ -3,7 +3,6 @@ const Workspace = use('App/Models/Workspace'); const { validateAll, } = use('Validator'); -const Env = use('Env'); const btoa = require('btoa'); const fetch = require('node-fetch'); @@ -11,7 +10,7 @@ const uuid = require('uuid/v4'); const crypto = require('crypto'); const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => { - const base = url + '/v1/'; + const base = `${url}/v1/`; const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; try { @@ -126,7 +125,7 @@ class UserController { const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); - const base = server + '/v1/'; + const base = `${server}/v1/`; const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; // Try to get an authentication token -- cgit v1.2.3-70-g09d2 From 1cfff4a4324e130aa7579ea8694438ade686dd55 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Fri, 18 Oct 2019 21:43:42 +0200 Subject: Move internal server to submodule --- src/server/.editorconfig | 13 -- src/server/.eslintrc.js | 22 -- src/server/.gitattributes | 2 - src/server/.gitignore | 19 -- src/server/README.md | 17 -- src/server/ace | 21 -- .../app/Controllers/Http/RecipeController.js | 119 ---------- .../app/Controllers/Http/ServiceController.js | 211 ------------------ .../app/Controllers/Http/StaticController.js | 224 ------------------- src/server/app/Controllers/Http/UserController.js | 228 ------------------- .../app/Controllers/Http/WorkspaceController.js | 148 ------------- src/server/app/Exceptions/Handler.js | 45 ---- .../app/Middleware/ConvertEmptyStringsToNull.js | 16 -- src/server/app/Models/Recipe.js | 8 - src/server/app/Models/Service.js | 8 - src/server/app/Models/Token.js | 8 - src/server/app/Models/Traits/NoTimestamp.js | 15 -- src/server/app/Models/User.js | 8 - src/server/app/Models/Workspace.js | 8 - src/server/config/app.js | 242 --------------------- src/server/config/auth.js | 93 -------- src/server/config/bodyParser.js | 156 ------------- src/server/config/cors.js | 86 -------- src/server/config/database.js | 83 ------- src/server/config/drive.js | 45 ---- src/server/config/hash.js | 48 ---- src/server/config/session.js | 98 --------- src/server/config/shield.js | 144 ------------ src/server/database/factory.js | 20 -- .../migrations/1566385379883_service_schema.js | 22 -- .../migrations/1566554231482_recipe_schema.js | 21 -- .../migrations/1566554359294_workspace_schema.js | 23 -- src/server/database/template.sqlite | Bin 36864 -> 0 bytes src/server/env.ini | 16 -- src/server/logo.png | Bin 298671 -> 0 bytes src/server/package.json | 49 ----- src/server/public/css/main.css | 69 ------ src/server/public/css/vanilla.css | 138 ------------ src/server/resources/views/import.edge | 18 -- src/server/resources/views/index.edge | 32 --- src/server/resources/views/layouts/main.edge | 18 -- src/server/start.js | 41 ---- src/server/start/app.js | 62 ------ src/server/start/kernel.js | 56 ----- src/server/start/routes.js | 74 ------- 45 files changed, 2794 deletions(-) delete mode 100644 src/server/.editorconfig delete mode 100644 src/server/.eslintrc.js delete mode 100644 src/server/.gitattributes delete mode 100644 src/server/.gitignore delete mode 100644 src/server/README.md delete mode 100644 src/server/ace delete mode 100644 src/server/app/Controllers/Http/RecipeController.js delete mode 100644 src/server/app/Controllers/Http/ServiceController.js delete mode 100644 src/server/app/Controllers/Http/StaticController.js delete mode 100644 src/server/app/Controllers/Http/UserController.js delete mode 100644 src/server/app/Controllers/Http/WorkspaceController.js delete mode 100644 src/server/app/Exceptions/Handler.js delete mode 100644 src/server/app/Middleware/ConvertEmptyStringsToNull.js delete mode 100644 src/server/app/Models/Recipe.js delete mode 100644 src/server/app/Models/Service.js delete mode 100644 src/server/app/Models/Token.js delete mode 100644 src/server/app/Models/Traits/NoTimestamp.js delete mode 100644 src/server/app/Models/User.js delete mode 100644 src/server/app/Models/Workspace.js delete mode 100644 src/server/config/app.js delete mode 100644 src/server/config/auth.js delete mode 100644 src/server/config/bodyParser.js delete mode 100644 src/server/config/cors.js delete mode 100644 src/server/config/database.js delete mode 100644 src/server/config/drive.js delete mode 100644 src/server/config/hash.js delete mode 100644 src/server/config/session.js delete mode 100644 src/server/config/shield.js delete mode 100644 src/server/database/factory.js delete mode 100644 src/server/database/migrations/1566385379883_service_schema.js delete mode 100644 src/server/database/migrations/1566554231482_recipe_schema.js delete mode 100644 src/server/database/migrations/1566554359294_workspace_schema.js delete mode 100644 src/server/database/template.sqlite delete mode 100644 src/server/env.ini delete mode 100644 src/server/logo.png delete mode 100644 src/server/package.json delete mode 100644 src/server/public/css/main.css delete mode 100644 src/server/public/css/vanilla.css delete mode 100644 src/server/resources/views/import.edge delete mode 100644 src/server/resources/views/index.edge delete mode 100644 src/server/resources/views/layouts/main.edge delete mode 100644 src/server/start.js delete mode 100644 src/server/start/app.js delete mode 100644 src/server/start/kernel.js delete mode 100644 src/server/start/routes.js (limited to 'src/server') diff --git a/src/server/.editorconfig b/src/server/.editorconfig deleted file mode 100644 index 914223976..000000000 --- a/src/server/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# editorconfig.org -root = true - -[*] -indent_size = 2 -indent_style = space -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/src/server/.eslintrc.js b/src/server/.eslintrc.js deleted file mode 100644 index d02f4890d..000000000 --- a/src/server/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - env: { - commonjs: true, - es6: true, - node: true, - }, - extends: [ - 'airbnb-base', - ], - globals: { - Atomics: 'readonly', - SharedArrayBuffer: 'readonly', - use: 'readonly' - }, - parserOptions: { - ecmaVersion: 2018, - }, - rules: { - "class-methods-use-this": 'off', - "no-restricted-syntax": 'off', - }, -}; diff --git a/src/server/.gitattributes b/src/server/.gitattributes deleted file mode 100644 index dfe077042..000000000 --- a/src/server/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/src/server/.gitignore b/src/server/.gitignore deleted file mode 100644 index d84ffadd4..000000000 --- a/src/server/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Node modules -node_modules - -# Adonis directory for storing tmp files -tmp - -# Environment variables, never commit this file -.env - -# The development sqlite file -database/development.sqlite -database/adonis.sqlite - -# Uploaded recipes -recipes/ - -.DS_Store -public/terms.html -public/privacy.html diff --git a/src/server/README.md b/src/server/README.md deleted file mode 100644 index 0074f2314..000000000 --- a/src/server/README.md +++ /dev/null @@ -1,17 +0,0 @@ -

- -

- -# ferdi-internal-server -Internal Ferdi Server used for storing settings without logging into an external server. - -## Configuration -franz-server's configuration is saved inside the `env.ini` file. Besides AdonisJS's settings, ferdi-internal-server has the following custom settings: -- `CONNECT_WITH_FRANZ` (`true` or `false`, default: `true`): Whether to enable connections to the Franz server. By enabling this option, ferdi-server can: - - Show the full Franz recipe library instead of only custom recipes - - Import Franz accounts - -## Importing your Franz account -ferdi-server allows you to import your full Franz account, including all its settings. - -To import your Franz account, open `http://localhost:45569/import` in your browser and login using your Franz account details. ferdi-server will create a new user with the same credentials and copy your Franz settings, services and workspaces. diff --git a/src/server/ace b/src/server/ace deleted file mode 100644 index 42f8f10d1..000000000 --- a/src/server/ace +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -/* -|-------------------------------------------------------------------------- -| Ace Commands -|-------------------------------------------------------------------------- -| -| The ace file is just a regular Javascript file but with no extension. You -| can call `node ace` followed by the command name and it just works. -| -| Also you can use `adonis` followed by the command name, since the adonis -| global proxies all the ace commands. -| -*/ - -const { Ignitor } = require('@adonisjs/ignitor') - -new Ignitor(require('@adonisjs/fold')) - .appRoot(__dirname) - .fireAce() - .catch(console.error) diff --git a/src/server/app/Controllers/Http/RecipeController.js b/src/server/app/Controllers/Http/RecipeController.js deleted file mode 100644 index 71ac12c0f..000000000 --- a/src/server/app/Controllers/Http/RecipeController.js +++ /dev/null @@ -1,119 +0,0 @@ - -const Recipe = use('App/Models/Recipe'); -const Drive = use('Drive'); -const { - validateAll, -} = use('Validator'); -const Env = use('Env'); - -const fetch = require('node-fetch'); - -class RecipeController { - // List official and custom recipes - async list({ - response, - }) { - const officialRecipes = JSON.parse(await (await fetch('https://api.getferdi.com/v1/recipes')).text()); - const customRecipesArray = (await Recipe.all()).rows; - const customRecipes = customRecipesArray.map(recipe => ({ - id: recipe.recipeId, - name: recipe.name, - ...JSON.parse(recipe.data), - })); - - const recipes = [ - ...officialRecipes, - ...customRecipes, - ]; - - return response.send(recipes); - } - - // Search official and custom recipes - async search({ - request, - response, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - needle: 'required', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Please provide a needle', - messages: validation.messages(), - status: 401, - }); - } - - const needle = request.input('needle'); - - // Get results - let results; - - if (needle === 'ferdi:custom') { - const dbResults = (await Recipe.all()).toJSON(); - results = dbResults.map(recipe => ({ - id: recipe.recipeId, - name: recipe.name, - ...JSON.parse(recipe.data), - })); - } else { - let remoteResults = []; - if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq - remoteResults = JSON.parse(await (await fetch(`https://api.getferdi.com/v1/recipes/search?needle=${encodeURIComponent(needle)}`)).text()); - } - const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON(); - const localResults = localResultsArray.map(recipe => ({ - id: recipe.recipeId, - name: recipe.name, - ...JSON.parse(recipe.data), - })); - - results = [ - ...localResults, - ...remoteResults || [], - ]; - } - - return response.send(results); - } - - // Download a recipe - async download({ - response, - params, - }) { - // Validate user input - const validation = await validateAll(params, { - recipe: 'required|accepted', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Please provide a recipe ID', - messages: validation.messages(), - status: 401, - }); - } - - const service = params.recipe; - - // Check for invalid characters - if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) { - return response.send('Invalid recipe name'); - } - - // Check if recipe exists in recipes folder - if (await Drive.exists(`${service}.tar.gz`)) { - return response.send(await Drive.get(`${service}.tar.gz`)); - } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq - return response.redirect(`https://api.getferdi.com/v1/recipes/download/${service}`); - } - return response.status(400).send({ - message: 'Recipe not found', - code: 'recipe-not-found', - }); - } -} - -module.exports = RecipeController; diff --git a/src/server/app/Controllers/Http/ServiceController.js b/src/server/app/Controllers/Http/ServiceController.js deleted file mode 100644 index ea7035ca1..000000000 --- a/src/server/app/Controllers/Http/ServiceController.js +++ /dev/null @@ -1,211 +0,0 @@ -const Service = use('App/Models/Service'); -const { - validateAll, -} = use('Validator'); - -const uuid = require('uuid/v4'); - -class ServiceController { - // Create a new service for user - async create({ - request, - response, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - name: 'required|string', - recipeId: 'required', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - const data = request.all(); - - // Get new, unused uuid - let serviceId; - do { - serviceId = uuid(); - } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop - - await Service.create({ - serviceId, - name: data.name, - recipeId: data.recipeId, - settings: JSON.stringify(data), - }); - - return response.send({ - data: { - userId: 1, - id: serviceId, - isEnabled: true, - isNotificationEnabled: true, - isBadgeEnabled: true, - isMuted: false, - isDarkModeEnabled: '', - spellcheckerLanguage: '', - order: 1, - customRecipe: false, - hasCustomIcon: false, - workspaces: [], - iconUrl: null, - ...data, - }, - status: ['created'], - }); - } - - // List all services a user has created - async list({ - response, - }) { - const services = (await Service.all()).rows; - // Convert to array with all data Franz wants - const servicesArray = services.map(service => ({ - customRecipe: false, - hasCustomIcon: false, - isBadgeEnabled: true, - isDarkModeEnabled: '', - isEnabled: true, - isMuted: false, - isNotificationEnabled: true, - order: 1, - spellcheckerLanguage: '', - workspaces: [], - iconUrl: null, - ...JSON.parse(service.settings), - id: service.serviceId, - name: service.name, - recipeId: service.recipeId, - userId: 1, - })); - - return response.send(servicesArray); - } - - async edit({ - request, - response, - params, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - name: 'required', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - const data = request.all(); - const { - id, - } = params; - - // Get current settings from db - const serviceData = (await Service.query() - .where('serviceId', id).fetch()).rows[0]; - - const settings = { - ...JSON.parse(serviceData.settings), - ...data, - }; - - // Update data in database - await (Service.query() - .where('serviceId', id)).update({ - name: data.name, - settings: JSON.stringify(settings), - }); - - // Get updated row - const service = (await Service.query() - .where('serviceId', id).fetch()).rows[0]; - - return response.send({ - id: service.serviceId, - name: data.name, - ...settings, - userId: 1, - }); - } - - async reorder({ - request, - response, - }) { - const data = request.all(); - - for (const service of Object.keys(data)) { - // Get current settings from db - const serviceData = (await Service.query() // eslint-disable-line no-await-in-loop - .where('serviceId', service).fetch()).rows[0]; - - const settings = { - ...JSON.parse(serviceData.settings), - order: data[service], - }; - - // Update data in database - await (Service.query() // eslint-disable-line no-await-in-loop - .where('serviceId', service)) - .update({ - settings: JSON.stringify(settings), - }); - } - - // Get new services - const services = (await Service.all()).rows; - // Convert to array with all data Franz wants - const servicesArray = services.map(service => ({ - customRecipe: false, - hasCustomIcon: false, - isBadgeEnabled: true, - isDarkModeEnabled: '', - isEnabled: true, - isMuted: false, - isNotificationEnabled: true, - order: 1, - spellcheckerLanguage: '', - workspaces: [], - iconUrl: null, - ...JSON.parse(service.settings), - id: service.serviceId, - name: service.name, - recipeId: service.recipeId, - userId: 1, - })); - - return response.send(servicesArray); - } - - update({ - response, - }) { - return response.send([]); - } - - async delete({ - params, - response, - }) { - // Update data in database - await (Service.query() - .where('serviceId', params.id)).delete(); - - return response.send({ - message: 'Sucessfully deleted service', - status: 200, - }); - } -} - -module.exports = ServiceController; diff --git a/src/server/app/Controllers/Http/StaticController.js b/src/server/app/Controllers/Http/StaticController.js deleted file mode 100644 index b16e6cb6d..000000000 --- a/src/server/app/Controllers/Http/StaticController.js +++ /dev/null @@ -1,224 +0,0 @@ - -/** - * Controller for routes with static responses - */ - -class StaticController { - // Enable all features - features({ - response, - }) { - return response.send({ - needToWaitToProceed: false, - isSpellcheckerPremiumFeature: true, - isServiceProxyEnabled: true, - isServiceProxyPremiumFeature: true, - isWorkspacePremiumFeature: true, - isWorkspaceEnabled: true, - isAnnouncementsEnabled: true, - isSettingsWSEnabled: false, - isServiceLimitEnabled: false, - serviceLimitCount: 0, - isCommunityRecipesPremiumFeature: false, - }); - } - - // Return an empty array - emptyArray({ - response, - }) { - return response.send([]); - } - - // Payment plans availible - plans({ - response, - }) { - return response.send({ - month: { - id: 'franz-supporter-license', - price: 99, - }, - year: { - id: 'franz-supporter-license-year-2019', - price: 99, - }, - }); - } - - // Return list of popular recipes (copy of the response Franz's API is returning) - popularRecipes({ - response, - }) { - return response.send([{ - author: 'Stefan Malzner ', - featured: false, - id: 'slack', - name: 'Slack', - version: '1.0.4', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/slack/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'whatsapp', - name: 'WhatsApp', - version: '1.0.1', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/whatsapp/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'messenger', - name: 'Messenger', - version: '1.0.6', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/messenger/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'telegram', - name: 'Telegram', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/telegram/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'gmail', - name: 'Gmail', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/gmail/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'skype', - name: 'Skype', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/skype/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'hangouts', - name: 'Hangouts', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/hangouts/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'discord', - name: 'Discord', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/discord/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'tweetdeck', - name: 'Tweetdeck', - version: '1.0.1', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/tweetdeck/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'hipchat', - name: 'HipChat', - version: '1.0.1', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/hipchat/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'gmailinbox', - name: 'Inbox by Gmail', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/gmailinbox/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'rocketchat', - name: 'Rocket.Chat', - version: '1.0.1', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/rocketchat/src/icon.svg', - }, - }, { - author: 'Brian Gilbert ', - featured: false, - id: 'gitter', - name: 'Gitter', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/gitter/src/icon.svg', - }, - }, { - author: 'Stefan Malzner ', - featured: false, - id: 'mattermost', - name: 'Mattermost', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/mattermost/src/icon.svg', - }, - }, { - author: 'Franz ', - featured: false, - id: 'toggl', - name: 'toggl', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/toggl/src/icon.svg', - }, - }, { - author: 'Stuart Clark ', - featured: false, - id: 'twist', - name: 'twist', - version: '1.0.0', - icons: { - png: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.png', - svg: 'https://cdn.franzinfra.com/recipes/dist/twist/src/icon.svg', - }, - }]); - } - - // Show announcements - announcement({ - response, - }) { - return response.send('No announcement found.'); - } -} - -module.exports = StaticController; diff --git a/src/server/app/Controllers/Http/UserController.js b/src/server/app/Controllers/Http/UserController.js deleted file mode 100644 index 07e118afd..000000000 --- a/src/server/app/Controllers/Http/UserController.js +++ /dev/null @@ -1,228 +0,0 @@ -const Service = use('App/Models/Service'); -const Workspace = use('App/Models/Workspace'); -const { - validateAll, -} = use('Validator'); - -const btoa = require('btoa'); -const fetch = require('node-fetch'); -const uuid = require('uuid/v4'); -const crypto = require('crypto'); - -const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => { - const base = `${url}/v1/`; - const user = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; - - try { - fetch(base + route, { - method, - headers: { - Authorization: `Bearer ${auth}`, - 'User-Agent': user, - }, - }) - .then(data => data.json()) - .then(json => resolve(json)); - } catch (e) { - reject(); - } -}); - -class UserController { - // Register a new user - async signup({ - request, - response, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - firstname: 'required', - email: 'required|email', - password: 'required', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - return response.send({ - message: 'Successfully created account', - token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M', - }); - } - - // Login using an existing user - async login({ - request, - response, - }) { - if (!request.header('Authorization')) { - return response.status(401).send({ - message: 'Please provide authorization', - status: 401, - }); - } - - return response.send({ - message: 'Successfully logged in', - token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M', - }); - } - - // Return information about the current user - async me({ - response, - }) { - return response.send({ - accountType: 'individual', - beta: false, - donor: {}, - email: '', - emailValidated: true, - features: {}, - firstname: 'Ferdi', - id: '82c1cf9d-ab58-4da2-b55e-aaa41d2142d8', - isPremium: true, - isSubscriptionOwner: true, - lastname: 'Application', - locale: 'en-US', - }); - } - - - async import({ - request, - response, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - email: 'required|email', - password: 'required', - server: 'required', - }); - if (validation.fails()) { - let errorMessage = 'There was an error while trying to import your account:\n'; - for (const message of validation.messages()) { - if (message.validation === 'required') { - errorMessage += `- Please make sure to supply your ${message.field}\n`; - } else if (message.validation === 'unique') { - errorMessage += '- There is already a user with this email.\n'; - } else { - errorMessage += `${message.message}\n`; - } - } - return response.status(401).send(errorMessage); - } - - const { - email, - password, - server, - } = request.all(); - - const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); - - const base = `${server}/v1/`; - const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Ferdi/5.3.0-beta.1 Chrome/69.0.3497.128 Electron/4.2.4 Safari/537.36'; - - // Try to get an authentication token - let token; - try { - const basicToken = btoa(`${email}:${hashedPassword}`); - - const rawResponse = await fetch(`${base}auth/login`, { - method: 'POST', - headers: { - Authorization: `Basic ${basicToken}`, - 'User-Agent': userAgent, - }, - }); - const content = await rawResponse.json(); - - if (!content.message || content.message !== 'Successfully logged in') { - const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again'; - return response.status(401).send(errorMessage); - } - - // eslint-disable-next-line prefer-destructuring - token = content.token; - } catch (e) { - return response.status(401).send({ - message: 'Cannot login to Franz', - error: e, - }); - } - - // Get user information - let userInf = false; - try { - userInf = await apiRequest(server, 'me', 'GET', token); - } catch (e) { - const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`; - return response.status(401).send(errorMessage); - } - if (!userInf) { - const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later'; - return response.status(401).send(errorMessage); - } - - const serviceIdTranslation = {}; - - // Import services - try { - const services = await apiRequest(server, 'me/services', 'GET', token); - - for (const service of services) { - // Get new, unused uuid - let serviceId; - do { - serviceId = uuid(); - } while ((await Service.query().where('serviceId', serviceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop - - await Service.create({ // eslint-disable-line no-await-in-loop - serviceId, - name: service.name, - recipeId: service.recipeId, - settings: JSON.stringify(service), - }); - - serviceIdTranslation[service.id] = serviceId; - } - } catch (e) { - const errorMessage = `Could not import your services into our system.\nError: ${e}`; - return response.status(401).send(errorMessage); - } - - // Import workspaces - try { - const workspaces = await apiRequest(server, 'workspace', 'GET', token); - - for (const workspace of workspaces) { - let workspaceId; - do { - workspaceId = uuid(); - } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop - - const services = workspace.services.map(service => serviceIdTranslation[service]); - - await Workspace.create({ // eslint-disable-line no-await-in-loop - workspaceId, - name: workspace.name, - order: workspace.order, - services: JSON.stringify(services), - data: JSON.stringify({}), - }); - } - } catch (e) { - const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; - return response.status(401).send(errorMessage); - } - - return response.send('Your account has been imported. You can now use your Franz account in Ferdi.'); - } -} - -module.exports = UserController; diff --git a/src/server/app/Controllers/Http/WorkspaceController.js b/src/server/app/Controllers/Http/WorkspaceController.js deleted file mode 100644 index 7990b8434..000000000 --- a/src/server/app/Controllers/Http/WorkspaceController.js +++ /dev/null @@ -1,148 +0,0 @@ -const Workspace = use('App/Models/Workspace'); -const { - validateAll, -} = use('Validator'); - -const uuid = require('uuid/v4'); - -class WorkspaceController { - // Create a new workspace for user - async create({ - request, - response, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - name: 'required|alpha', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - const data = request.all(); - - // Get new, unused uuid - let workspaceId; - do { - workspaceId = uuid(); - } while ((await Workspace.query().where('workspaceId', workspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop - - const order = (await Workspace.all()).rows.length; - - await Workspace.create({ - workspaceId, - name: data.name, - order, - services: JSON.stringify([]), - data: JSON.stringify(data), - }); - - return response.send({ - userId: 1, - name: data.name, - id: workspaceId, - order, - workspaces: [], - }); - } - - async edit({ - request, - response, - params, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - name: 'required|alpha', - services: 'required|array', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - const data = request.all(); - const { - id, - } = params; - - // Update data in database - await (Workspace.query() - .where('workspaceId', id)).update({ - name: data.name, - services: JSON.stringify(data.services), - }); - - // Get updated row - const workspace = (await Workspace.query() - .where('workspaceId', id).fetch()).rows[0]; - - return response.send({ - id: workspace.workspaceId, - name: data.name, - order: workspace.order, - services: data.services, - userId: 1, - }); - } - - async delete({ - request, - response, - params, - }) { - // Validate user input - const validation = await validateAll(request.all(), { - id: 'required', - }); - if (validation.fails()) { - return response.status(401).send({ - message: 'Invalid POST arguments', - messages: validation.messages(), - status: 401, - }); - } - - const { - id, - } = params; - - // Update data in database - await (Workspace.query() - .where('workspaceId', id)).delete(); - - return response.send({ - message: 'Successfully deleted workspace', - }); - } - - // List all workspaces a user has created - async list({ - response, - }) { - const workspaces = (await Workspace.all()).rows; - // Convert to array with all data Franz wants - let workspacesArray = []; - if (workspaces) { - workspacesArray = workspaces.map(workspace => ({ - id: workspace.workspaceId, - name: workspace.name, - order: workspace.order, - services: JSON.parse(workspace.services), - userId: 1, - })); - } - - - return response.send(workspacesArray); - } -} - -module.exports = WorkspaceController; diff --git a/src/server/app/Exceptions/Handler.js b/src/server/app/Exceptions/Handler.js deleted file mode 100644 index e8d2d2ee2..000000000 --- a/src/server/app/Exceptions/Handler.js +++ /dev/null @@ -1,45 +0,0 @@ - -const BaseExceptionHandler = use('BaseExceptionHandler'); - -/** - * This class handles all exceptions thrown during - * the HTTP request lifecycle. - * - * @class ExceptionHandler - */ -class ExceptionHandler extends BaseExceptionHandler { - /** - * Handle exception thrown during the HTTP lifecycle - * - * @method handle - * - * @param {Object} error - * @param {Object} options.request - * @param {Object} options.response - * - * @return {void} - */ - async handle(error, { response }) { - if (error.name === 'ValidationException') { - return response.status(400).send('Invalid arguments'); - } - - return response.status(error.status).send(error.message); - } - - /** - * Report exception for logging or debugging. - * - * @method report - * - * @param {Object} error - * @param {Object} options.request - * - * @return {void} - */ - async report() { - return true; - } -} - -module.exports = ExceptionHandler; diff --git a/src/server/app/Middleware/ConvertEmptyStringsToNull.js b/src/server/app/Middleware/ConvertEmptyStringsToNull.js deleted file mode 100644 index bc3079a7f..000000000 --- a/src/server/app/Middleware/ConvertEmptyStringsToNull.js +++ /dev/null @@ -1,16 +0,0 @@ - -class ConvertEmptyStringsToNull { - async handle({ request }, next) { - if (Object.keys(request.body).length) { - request.body = Object.assign( - ...Object.keys(request.body).map(key => ({ - [key]: request.body[key] !== '' ? request.body[key] : null, - })), - ); - } - - await next(); - } -} - -module.exports = ConvertEmptyStringsToNull; diff --git a/src/server/app/Models/Recipe.js b/src/server/app/Models/Recipe.js deleted file mode 100644 index da3618bf7..000000000 --- a/src/server/app/Models/Recipe.js +++ /dev/null @@ -1,8 +0,0 @@ - -/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ -const Model = use('Model'); - -class Recipe extends Model { -} - -module.exports = Recipe; diff --git a/src/server/app/Models/Service.js b/src/server/app/Models/Service.js deleted file mode 100644 index 20679feb1..000000000 --- a/src/server/app/Models/Service.js +++ /dev/null @@ -1,8 +0,0 @@ - -/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ -const Model = use('Model'); - -class Service extends Model { -} - -module.exports = Service; diff --git a/src/server/app/Models/Token.js b/src/server/app/Models/Token.js deleted file mode 100644 index f6bec0852..000000000 --- a/src/server/app/Models/Token.js +++ /dev/null @@ -1,8 +0,0 @@ - -/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ -const Model = use('Model'); - -class Token extends Model { -} - -module.exports = Token; diff --git a/src/server/app/Models/Traits/NoTimestamp.js b/src/server/app/Models/Traits/NoTimestamp.js deleted file mode 100644 index c647428b3..000000000 --- a/src/server/app/Models/Traits/NoTimestamp.js +++ /dev/null @@ -1,15 +0,0 @@ - -class NoTimestamp { - register(Model) { - Object.defineProperties(Model, { - createdAtColumn: { - get: () => null, - }, - updatedAtColumn: { - get: () => null, - }, - }); - } -} - -module.exports = NoTimestamp; diff --git a/src/server/app/Models/User.js b/src/server/app/Models/User.js deleted file mode 100644 index 907710d8d..000000000 --- a/src/server/app/Models/User.js +++ /dev/null @@ -1,8 +0,0 @@ -// File is required by AdonisJS but not used by the server -/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ -const Model = use('Model'); - -class User extends Model { -} - -module.exports = User; diff --git a/src/server/app/Models/Workspace.js b/src/server/app/Models/Workspace.js deleted file mode 100644 index 3b73cbf33..000000000 --- a/src/server/app/Models/Workspace.js +++ /dev/null @@ -1,8 +0,0 @@ - -/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ -const Model = use('Model'); - -class Workspace extends Model { -} - -module.exports = Workspace; diff --git a/src/server/config/app.js b/src/server/config/app.js deleted file mode 100644 index 7938b81df..000000000 --- a/src/server/config/app.js +++ /dev/null @@ -1,242 +0,0 @@ - -/** @type {import('@adonisjs/framework/src/Env')} */ -const Env = use('Env'); - -module.exports = { - - /* - |-------------------------------------------------------------------------- - | Application Name - |-------------------------------------------------------------------------- - | - | This value is the name of your application and can used when you - | need to place the application's name in a email, view or - | other location. - | - */ - - name: Env.get('APP_NAME', 'Ferdi Internal Server'), - - /* - |-------------------------------------------------------------------------- - | App Key - |-------------------------------------------------------------------------- - | - | App key is a randomly generated 16 or 32 characters long string required - | to encrypt cookies, sessions and other sensitive data. - | - */ - appKey: Env.getOrFail('APP_KEY'), - - http: { - /* - |-------------------------------------------------------------------------- - | Allow Method Spoofing - |-------------------------------------------------------------------------- - | - | Method spoofing allows to make requests by spoofing the http verb. - | Which means you can make a GET request but instruct the server to - | treat as a POST or PUT request. If you want this feature, set the - | below value to true. - | - */ - allowMethodSpoofing: true, - - /* - |-------------------------------------------------------------------------- - | Trust Proxy - |-------------------------------------------------------------------------- - | - | Trust proxy defines whether X-Forwarded-* headers should be trusted or not. - | When your application is behind a proxy server like nginx, these values - | are set automatically and should be trusted. Apart from setting it - | to true or false Adonis supports handful or ways to allow proxy - | values. Read documentation for that. - | - */ - trustProxy: false, - - /* - |-------------------------------------------------------------------------- - | Subdomains - |-------------------------------------------------------------------------- - | - | Offset to be used for returning subdomains for a given request.For - | majority of applications it will be 2, until you have nested - | sudomains. - | cheatsheet.adonisjs.com - offset - 2 - | virk.cheatsheet.adonisjs.com - offset - 3 - | - */ - subdomainOffset: 2, - - /* - |-------------------------------------------------------------------------- - | JSONP Callback - |-------------------------------------------------------------------------- - | - | Default jsonp callback to be used when callback query string is missing - | in request url. - | - */ - jsonpCallback: 'callback', - - - /* - |-------------------------------------------------------------------------- - | Etag - |-------------------------------------------------------------------------- - | - | Set etag on all HTTP response. In order to disable for selected routes, - | you can call the `response.send` with an options object as follows. - | - | response.send('Hello', { ignoreEtag: true }) - | - */ - etag: false, - }, - - views: { - /* - |-------------------------------------------------------------------------- - | Cache Views - |-------------------------------------------------------------------------- - | - | Define whether or not to cache the compiled view. Set it to true in - | production to optimize view loading time. - | - */ - cache: Env.get('CACHE_VIEWS', true), - }, - - static: { - /* - |-------------------------------------------------------------------------- - | Dot Files - |-------------------------------------------------------------------------- - | - | Define how to treat dot files when trying to server static resources. - | By default it is set to ignore, which will pretend that dotfiles - | does not exists. - | - | Can be one of the following - | ignore, deny, allow - | - */ - dotfiles: 'ignore', - - /* - |-------------------------------------------------------------------------- - | ETag - |-------------------------------------------------------------------------- - | - | Enable or disable etag generation - | - */ - etag: true, - - /* - |-------------------------------------------------------------------------- - | Extensions - |-------------------------------------------------------------------------- - | - | Set file extension fallbacks. When set, if a file is not found, the given - | extensions will be added to the file name and search for. The first - | that exists will be served. Example: ['html', 'htm']. - | - */ - extensions: false, - }, - - locales: { - /* - |-------------------------------------------------------------------------- - | Loader - |-------------------------------------------------------------------------- - | - | The loader to be used for fetching and updating locales. Below is the - | list of available options. - | - | file, database - | - */ - loader: 'file', - - /* - |-------------------------------------------------------------------------- - | Default Locale - |-------------------------------------------------------------------------- - | - | Default locale to be used by Antl provider. You can always switch drivers - | in runtime or use the official Antl middleware to detect the driver - | based on HTTP headers/query string. - | - */ - locale: 'en', - }, - - logger: { - /* - |-------------------------------------------------------------------------- - | Transport - |-------------------------------------------------------------------------- - | - | Transport to be used for logging messages. You can have multiple - | transports using same driver. - | - | Available drivers are: `file` and `console`. - | - */ - transport: 'console', - - /* - |-------------------------------------------------------------------------- - | Console Transport - |-------------------------------------------------------------------------- - | - | Using `console` driver for logging. This driver writes to `stdout` - | and `stderr` - | - */ - console: { - driver: 'console', - name: 'adonis-app', - level: 'info', - }, - - /* - |-------------------------------------------------------------------------- - | File Transport - |-------------------------------------------------------------------------- - | - | File transport uses file driver and writes log messages for a given - | file inside `tmp` directory for your app. - | - | For a different directory, set an absolute path for the filename. - | - */ - file: { - driver: 'file', - name: 'adonis-app', - filename: 'adonis.log', - level: 'info', - }, - }, - - /* - |-------------------------------------------------------------------------- - | Generic Cookie Options - |-------------------------------------------------------------------------- - | - | The following cookie options are generic settings used by AdonisJs to create - | cookies. However, some parts of the application like `sessions` can have - | separate settings for cookies inside `config/session.js`. - | - */ - cookie: { - httpOnly: true, - sameSite: false, - path: '/', - maxAge: 7200, - }, -}; diff --git a/src/server/config/auth.js b/src/server/config/auth.js deleted file mode 100644 index b831b06c6..000000000 --- a/src/server/config/auth.js +++ /dev/null @@ -1,93 +0,0 @@ - -/** @type {import('@adonisjs/framework/src/Env')} */ -const Env = use('Env'); - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Authenticator - |-------------------------------------------------------------------------- - | - | Authentication is a combination of serializer and scheme with extra - | config to define on how to authenticate a user. - | - | Available Schemes - basic, session, jwt, api - | Available Serializers - lucid, database - | - */ - authenticator: 'jwt', - - /* - |-------------------------------------------------------------------------- - | Session - |-------------------------------------------------------------------------- - | - | Session authenticator makes use of sessions to authenticate a user. - | Session authentication is always persistent. - | - */ - session: { - serializer: 'lucid', - model: 'App/Models/User', - scheme: 'session', - uid: 'email', - password: 'password', - }, - - /* - |-------------------------------------------------------------------------- - | Basic Auth - |-------------------------------------------------------------------------- - | - | The basic auth authenticator uses basic auth header to authenticate a - | user. - | - | NOTE: - | This scheme is not persistent and users are supposed to pass - | login credentials on each request. - | - */ - basic: { - serializer: 'lucid', - model: 'App/Models/User', - scheme: 'basic', - uid: 'email', - password: 'password', - }, - - /* - |-------------------------------------------------------------------------- - | Jwt - |-------------------------------------------------------------------------- - | - | The jwt authenticator works by passing a jwt token on each HTTP request - | via HTTP `Authorization` header. - | - */ - jwt: { - serializer: 'lucid', - model: 'App/Models/User', - scheme: 'jwt', - uid: 'email', - password: 'password', - options: { - secret: Env.get('APP_KEY'), - }, - }, - - /* - |-------------------------------------------------------------------------- - | Api - |-------------------------------------------------------------------------- - | - | The Api scheme makes use of API personal tokens to authenticate a user. - | - */ - api: { - serializer: 'lucid', - model: 'App/Models/User', - scheme: 'api', - uid: 'email', - password: 'password', - }, -}; diff --git a/src/server/config/bodyParser.js b/src/server/config/bodyParser.js deleted file mode 100644 index c336e67d2..000000000 --- a/src/server/config/bodyParser.js +++ /dev/null @@ -1,156 +0,0 @@ - -module.exports = { - /* - |-------------------------------------------------------------------------- - | JSON Parser - |-------------------------------------------------------------------------- - | - | Below settings are applied when the request body contains a JSON payload. - | If you want body parser to ignore JSON payloads, then simply set `types` - | to an empty array. - */ - json: { - /* - |-------------------------------------------------------------------------- - | limit - |-------------------------------------------------------------------------- - | - | Defines the limit of JSON that can be sent by the client. If payload - | is over 1mb it will not be processed. - | - */ - limit: '50mb', - - /* - |-------------------------------------------------------------------------- - | strict - |-------------------------------------------------------------------------- - | - | When `strict` is set to true, body parser will only parse Arrays and - | Object. Otherwise everything parseable by `JSON.parse` is parsed. - | - */ - strict: true, - - /* - |-------------------------------------------------------------------------- - | types - |-------------------------------------------------------------------------- - | - | Which content types are processed as JSON payloads. You are free to - | add your own types here, but the request body should be parseable - | by `JSON.parse` method. - | - */ - types: [ - 'application/json', - 'application/json-patch+json', - 'application/vnd.api+json', - 'application/csp-report', - ], - }, - - /* - |-------------------------------------------------------------------------- - | Raw Parser - |-------------------------------------------------------------------------- - | - | - | - */ - raw: { - types: [ - 'text/*', - ], - }, - - /* - |-------------------------------------------------------------------------- - | Form Parser - |-------------------------------------------------------------------------- - | - | - | - */ - form: { - types: [ - 'application/x-www-form-urlencoded', - ], - }, - - /* - |-------------------------------------------------------------------------- - | Files Parser - |-------------------------------------------------------------------------- - | - | - | - */ - files: { - types: [ - 'multipart/form-data', - ], - - /* - |-------------------------------------------------------------------------- - | Max Size - |-------------------------------------------------------------------------- - | - | Below value is the max size of all the files uploaded to the server. It - | is validated even before files have been processed and hard exception - | is thrown. - | - | Consider setting a reasonable value here, otherwise people may upload GB's - | of files which will keep your server busy. - | - | Also this value is considered when `autoProcess` is set to true. - | - */ - maxSize: '20mb', - - /* - |-------------------------------------------------------------------------- - | Auto Process - |-------------------------------------------------------------------------- - | - | Whether or not to auto-process files. Since HTTP servers handle files via - | couple of specific endpoints. It is better to set this value off and - | manually process the files when required. - | - | This value can contain a boolean or an array of route patterns - | to be autoprocessed. - */ - autoProcess: true, - - /* - |-------------------------------------------------------------------------- - | Process Manually - |-------------------------------------------------------------------------- - | - | The list of routes that should not process files and instead rely on - | manual process. This list should only contain routes when autoProcess - | is to true. Otherwise everything is processed manually. - | - */ - processManually: [], - - /* - |-------------------------------------------------------------------------- - | Temporary file name - |-------------------------------------------------------------------------- - | - | Define a function, which should return a string to be used as the - | tmp file name. - | - | If not defined, Bodyparser will use `uuid` as the tmp file name. - | - | To be defined as. If you are defining the function, then do make sure - | to return a value from it. - | - | tmpFileName () { - | return 'some-unique-value' - | } - | - */ - }, -}; diff --git a/src/server/config/cors.js b/src/server/config/cors.js deleted file mode 100644 index 7ebbe3ffa..000000000 --- a/src/server/config/cors.js +++ /dev/null @@ -1,86 +0,0 @@ - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Origin - |-------------------------------------------------------------------------- - | - | Set a list of origins to be allowed. The value can be one of the following - | - | Boolean: true - Allow current request origin - | Boolean: false - Disallow all - | String - Comma separated list of allowed origins - | Array - An array of allowed origins - | String: * - A wildcard to allow current request origin - | Function - Receives the current origin and should return one of the above values. - | - */ - origin: false, - - /* - |-------------------------------------------------------------------------- - | Methods - |-------------------------------------------------------------------------- - | - | HTTP methods to be allowed. The value can be one of the following - | - | String - Comma separated list of allowed methods - | Array - An array of allowed methods - | - */ - methods: ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'], - - /* - |-------------------------------------------------------------------------- - | Headers - |-------------------------------------------------------------------------- - | - | List of headers to be allowed via Access-Control-Request-Headers header. - | The value can be one of the following. - | - | Boolean: true - Allow current request headers - | Boolean: false - Disallow all - | String - Comma separated list of allowed headers - | Array - An array of allowed headers - | String: * - A wildcard to allow current request headers - | Function - Receives the current header and should return one of the above values. - | - */ - headers: true, - - /* - |-------------------------------------------------------------------------- - | Expose Headers - |-------------------------------------------------------------------------- - | - | A list of headers to be exposed via `Access-Control-Expose-Headers` - | header. The value can be one of the following. - | - | Boolean: false - Disallow all - | String: Comma separated list of allowed headers - | Array - An array of allowed headers - | - */ - exposeHeaders: false, - - /* - |-------------------------------------------------------------------------- - | Credentials - |-------------------------------------------------------------------------- - | - | Define Access-Control-Allow-Credentials header. It should always be a - | boolean. - | - */ - credentials: false, - - /* - |-------------------------------------------------------------------------- - | MaxAge - |-------------------------------------------------------------------------- - | - | Define Access-Control-Allow-Max-Age - | - */ - maxAge: 90, -}; diff --git a/src/server/config/database.js b/src/server/config/database.js deleted file mode 100644 index a413f7050..000000000 --- a/src/server/config/database.js +++ /dev/null @@ -1,83 +0,0 @@ - -/** @type {import('@adonisjs/framework/src/Env')} */ -const Env = use('Env'); - -const dbPath = process.env.DB_PATH; - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Default Connection - |-------------------------------------------------------------------------- - | - | Connection defines the default connection settings to be used while - | interacting with SQL databases. - | - */ - connection: Env.get('DB_CONNECTION', 'sqlite'), - - /* - |-------------------------------------------------------------------------- - | Sqlite - |-------------------------------------------------------------------------- - | - | Sqlite is a flat file database and can be a good choice for a development - | environment. - | - | npm i --save sqlite3 - | - */ - sqlite: { - client: 'sqlite3', - connection: { - // filename: Helpers.databasePath(`${Env.get('DB_DATABASE', 'development')}.sqlite`), - filename: dbPath, - }, - useNullAsDefault: true, - debug: Env.get('DB_DEBUG', false), - }, - - /* - |-------------------------------------------------------------------------- - | MySQL - |-------------------------------------------------------------------------- - | - | Here we define connection settings for MySQL database. - | - | npm i --save mysql - | - */ - mysql: { - client: 'mysql', - connection: { - host: Env.get('DB_HOST', 'localhost'), - port: Env.get('DB_PORT', ''), - user: Env.get('DB_USER', 'root'), - password: Env.get('DB_PASSWORD', ''), - database: Env.get('DB_DATABASE', 'adonis'), - }, - debug: Env.get('DB_DEBUG', false), - }, - - /* - |-------------------------------------------------------------------------- - | PostgreSQL - |-------------------------------------------------------------------------- - | - | Here we define connection settings for PostgreSQL database. - | - | npm i --save pg - | - */ - pg: { - client: 'pg', - connection: { - host: Env.get('DB_HOST', 'localhost'), - port: Env.get('DB_PORT', ''), - user: Env.get('DB_USER', 'root'), - password: Env.get('DB_PASSWORD', ''), - database: Env.get('DB_DATABASE', 'adonis'), - }, - debug: Env.get('DB_DEBUG', false), - }, -}; diff --git a/src/server/config/drive.js b/src/server/config/drive.js deleted file mode 100644 index 617ce470a..000000000 --- a/src/server/config/drive.js +++ /dev/null @@ -1,45 +0,0 @@ -const Env = use('Env'); - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Default disk - |-------------------------------------------------------------------------- - | - | The default disk is used when you interact with the file system without - | defining a disk name - | - */ - default: 'local', - - disks: { - /* - |-------------------------------------------------------------------------- - | Local - |-------------------------------------------------------------------------- - | - | Local disk interacts with the a local folder inside your application - | - */ - local: { - root: `${__dirname}/../recipes`, - driver: 'local', - }, - - /* - |-------------------------------------------------------------------------- - | S3 - |-------------------------------------------------------------------------- - | - | S3 disk interacts with a bucket on aws s3 - | - */ - s3: { - driver: 's3', - key: Env.get('S3_KEY'), - secret: Env.get('S3_SECRET'), - bucket: Env.get('S3_BUCKET'), - region: Env.get('S3_REGION'), - }, - }, -}; diff --git a/src/server/config/hash.js b/src/server/config/hash.js deleted file mode 100644 index 297c977fc..000000000 --- a/src/server/config/hash.js +++ /dev/null @@ -1,48 +0,0 @@ - -/** @type {import('@adonisjs/framework/src/Env')} */ -const Env = use('Env'); - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Driver - |-------------------------------------------------------------------------- - | - | Driver to be used for hashing values. The same driver is used by the - | auth module too. - | - */ - driver: Env.get('HASH_DRIVER', 'bcrypt'), - - /* - |-------------------------------------------------------------------------- - | Bcrypt - |-------------------------------------------------------------------------- - | - | Config related to bcrypt hashing. https://www.npmjs.com/package/bcrypt - | package is used internally. - | - */ - bcrypt: { - rounds: 10, - }, - - /* - |-------------------------------------------------------------------------- - | Argon - |-------------------------------------------------------------------------- - | - | Config related to argon. https://www.npmjs.com/package/argon2 package is - | used internally. - | - | Since argon is optional, you will have to install the dependency yourself - | - |============================================================================ - | npm i argon2 - |============================================================================ - | - */ - argon: { - type: 1, - }, -}; diff --git a/src/server/config/session.js b/src/server/config/session.js deleted file mode 100644 index bce28bdd9..000000000 --- a/src/server/config/session.js +++ /dev/null @@ -1,98 +0,0 @@ - -const Env = use('Env'); - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Session Driver - |-------------------------------------------------------------------------- - | - | The session driver to be used for storing session values. It can be - | cookie, file or redis. - | - | For `redis` driver, make sure to install and register `@adonisjs/redis` - | - */ - driver: Env.get('SESSION_DRIVER', 'cookie'), - - /* - |-------------------------------------------------------------------------- - | Cookie Name - |-------------------------------------------------------------------------- - | - | The name of the cookie to be used for saving session id. Session ids - | are signed and encrypted. - | - */ - cookieName: 'adonis-session', - - /* - |-------------------------------------------------------------------------- - | Clear session when browser closes - |-------------------------------------------------------------------------- - | - | If this value is true, the session cookie will be temporary and will be - | removed when browser closes. - | - */ - clearWithBrowser: true, - - /* - |-------------------------------------------------------------------------- - | Session age - |-------------------------------------------------------------------------- - | - | This value is only used when `clearWithBrowser` is set to false. The - | age must be a valid https://npmjs.org/package/ms string or should - | be in milliseconds. - | - | Valid values are: - | '2h', '10d', '5y', '2.5 hrs' - | - */ - age: '2h', - - /* - |-------------------------------------------------------------------------- - | Cookie options - |-------------------------------------------------------------------------- - | - | Cookie options defines the options to be used for setting up session - | cookie - | - */ - cookie: { - httpOnly: true, - path: '/', - sameSite: false, - }, - - /* - |-------------------------------------------------------------------------- - | Sessions location - |-------------------------------------------------------------------------- - | - | If driver is set to file, we need to define the relative location from - | the temporary path or absolute url to any location. - | - */ - file: { - location: 'sessions', - }, - - /* - |-------------------------------------------------------------------------- - | Redis config - |-------------------------------------------------------------------------- - | - | The configuration for the redis driver. - | - */ - redis: { - host: '127.0.0.1', - port: 6379, - password: null, - db: 0, - keyPrefix: '', - }, -}; diff --git a/src/server/config/shield.js b/src/server/config/shield.js deleted file mode 100644 index 5c1c5cd73..000000000 --- a/src/server/config/shield.js +++ /dev/null @@ -1,144 +0,0 @@ - -module.exports = { - /* - |-------------------------------------------------------------------------- - | Content Security Policy - |-------------------------------------------------------------------------- - | - | Content security policy filters out the origins not allowed to execute - | and load resources like scripts, styles and fonts. There are wide - | variety of options to choose from. - */ - csp: { - /* - |-------------------------------------------------------------------------- - | Directives - |-------------------------------------------------------------------------- - | - | All directives are defined in camelCase and here is the list of - | available directives and their possible values. - | - | https://content-security-policy.com - | - | @example - | directives: { - | defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com'] - | } - | - */ - directives: { - }, - /* - |-------------------------------------------------------------------------- - | Report only - |-------------------------------------------------------------------------- - | - | Setting `reportOnly=true` will not block the scripts from running and - | instead report them to a URL. - | - */ - reportOnly: false, - /* - |-------------------------------------------------------------------------- - | Set all headers - |-------------------------------------------------------------------------- - | - | Headers staring with `X` have been depreciated, since all major browsers - | supports the standard CSP header. So its better to disable deperciated - | headers, unless you want them to be set. - | - */ - setAllHeaders: false, - - /* - |-------------------------------------------------------------------------- - | Disable on android - |-------------------------------------------------------------------------- - | - | Certain versions of android are buggy with CSP policy. So you can set - | this value to true, to disable it for Android versions with buggy - | behavior. - | - | Here is an issue reported on a different package, but helpful to read - | if you want to know the behavior. https://github.com/helmetjs/helmet/pull/82 - | - */ - disableAndroid: true, - }, - - /* - |-------------------------------------------------------------------------- - | X-XSS-Protection - |-------------------------------------------------------------------------- - | - | X-XSS Protection saves from applications from XSS attacks. It is adopted - | by IE and later followed by some other browsers. - | - | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection - | - */ - xss: { - enabled: true, - enableOnOldIE: false, - }, - - /* - |-------------------------------------------------------------------------- - | Iframe Options - |-------------------------------------------------------------------------- - | - | xframe defines whether or not your website can be embedded inside an - | iframe. Choose from one of the following options. - | @available options - | DENY, SAMEORIGIN, ALLOW-FROM http://example.com - | - | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - */ - xframe: 'DENY', - - /* - |-------------------------------------------------------------------------- - | No Sniff - |-------------------------------------------------------------------------- - | - | Browsers have a habit of sniffing content-type of a response. Which means - | files with .txt extension containing Javascript code will be executed as - | Javascript. You can disable this behavior by setting nosniff to false. - | - | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - | - */ - nosniff: true, - - /* - |-------------------------------------------------------------------------- - | No Open - |-------------------------------------------------------------------------- - | - | IE users can execute webpages in the context of your website, which is - | a serious security risk. Below option will manage this for you. - | - */ - noopen: true, - - /* - |-------------------------------------------------------------------------- - | CSRF Protection - |-------------------------------------------------------------------------- - | - | CSRF Protection adds another layer of security by making sure, actionable - | routes does have a valid token to execute an action. - | - */ - csrf: { - enable: true, - methods: ['POST', 'PUT', 'DELETE'], - filterUris: [], - cookieOptions: { - httpOnly: false, - sameSite: true, - path: '/', - maxAge: 7200, - }, - }, -}; diff --git a/src/server/database/factory.js b/src/server/database/factory.js deleted file mode 100644 index 550c5e6ab..000000000 --- a/src/server/database/factory.js +++ /dev/null @@ -1,20 +0,0 @@ - -/* -|-------------------------------------------------------------------------- -| Factory -|-------------------------------------------------------------------------- -| -| Factories are used to define blueprints for database tables or Lucid -| models. Later you can use these blueprints to seed your database -| with dummy data. -| -*/ - -/** @type {import('@adonisjs/lucid/src/Factory')} */ -// const Factory = use('Factory') - -// Factory.blueprint('App/Models/User', (faker) => { -// return { -// username: faker.username() -// } -// }) diff --git a/src/server/database/migrations/1566385379883_service_schema.js b/src/server/database/migrations/1566385379883_service_schema.js deleted file mode 100644 index 1db95c19d..000000000 --- a/src/server/database/migrations/1566385379883_service_schema.js +++ /dev/null @@ -1,22 +0,0 @@ - -/** @type {import('@adonisjs/lucid/src/Schema')} */ -const Schema = use('Schema'); - -class ServiceSchema extends Schema { - up() { - this.create('services', (table) => { - table.increments(); - table.string('serviceId', 80).notNullable(); - table.string('name', 80).notNullable(); - table.string('recipeId', 254).notNullable(); - table.json('settings'); - table.timestamps(); - }); - } - - down() { - this.drop('services'); - } -} - -module.exports = ServiceSchema; diff --git a/src/server/database/migrations/1566554231482_recipe_schema.js b/src/server/database/migrations/1566554231482_recipe_schema.js deleted file mode 100644 index 14fcb82e5..000000000 --- a/src/server/database/migrations/1566554231482_recipe_schema.js +++ /dev/null @@ -1,21 +0,0 @@ - -/** @type {import('@adonisjs/lucid/src/Schema')} */ -const Schema = use('Schema'); - -class RecipeSchema extends Schema { - up() { - this.create('recipes', (table) => { - table.increments(); - table.string('name', 80).notNullable(); - table.string('recipeId', 254).notNullable().unique(); - table.json('data'); - table.timestamps(); - }); - } - - down() { - this.drop('recipes'); - } -} - -module.exports = RecipeSchema; diff --git a/src/server/database/migrations/1566554359294_workspace_schema.js b/src/server/database/migrations/1566554359294_workspace_schema.js deleted file mode 100644 index b53bbe656..000000000 --- a/src/server/database/migrations/1566554359294_workspace_schema.js +++ /dev/null @@ -1,23 +0,0 @@ - -/** @type {import('@adonisjs/lucid/src/Schema')} */ -const Schema = use('Schema'); - -class WorkspaceSchema extends Schema { - up() { - this.create('workspaces', (table) => { - table.increments(); - table.string('workspaceId', 80).notNullable().unique(); - table.string('name', 80).notNullable(); - table.integer('order'); - table.json('services'); - table.json('data'); - table.timestamps(); - }); - } - - down() { - this.drop('workspaces'); - } -} - -module.exports = WorkspaceSchema; diff --git a/src/server/database/template.sqlite b/src/server/database/template.sqlite deleted file mode 100644 index db5425ee6..000000000 Binary files a/src/server/database/template.sqlite and /dev/null differ diff --git a/src/server/env.ini b/src/server/env.ini deleted file mode 100644 index 902e8e4c8..000000000 --- a/src/server/env.ini +++ /dev/null @@ -1,16 +0,0 @@ -HOST=127.0.0.1 -PORT=45569 -NODE_ENV=development -APP_NAME=Ferdi Internal Server -APP_URL=http://${HOST}:${PORT} -CACHE_VIEWS=false -APP_KEY=FERDIINTERNALSERVER -DB_CONNECTION=sqlite -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_USER=root -DB_PASSWORD= -DB_DATABASE=ferdi -HASH_DRIVER=bcrypt -IS_CREATION_ENABLED=true -CONNECT_WITH_FRANZ=true \ No newline at end of file diff --git a/src/server/logo.png b/src/server/logo.png deleted file mode 100644 index 4145a077a..000000000 Binary files a/src/server/logo.png and /dev/null differ diff --git a/src/server/package.json b/src/server/package.json deleted file mode 100644 index 60dd58e93..000000000 --- a/src/server/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "ferdi-internal-server", - "version": "1.0.0", - "adonis-version": "4.1.0", - "description": "Internal server used by the Ferdi application.", - "main": "index.js", - "scripts": { - "start": "node server.js", - "test": "node ace test", - "lint": "eslint --fix ./" - }, - "keywords": [ - ], - "author": "", - "license": "MIT License", - "private": true, - "dependencies": { - "@adonisjs/ace": "^5.0.8", - "@adonisjs/auth": "^3.0.7", - "@adonisjs/bodyparser": "^2.0.5", - "@adonisjs/cors": "^1.0.7", - "@adonisjs/drive": "^1.0.4", - "@adonisjs/fold": "^4.0.9", - "@adonisjs/framework": "^5.0.9", - "@adonisjs/ignitor": "^2.0.8", - "@adonisjs/lucid": "^6.1.3", - "@adonisjs/session": "^1.0.29", - "@adonisjs/shield": "^1.0.8", - "@adonisjs/validator": "^5.0.6", - "atob": "^2.1.2", - "btoa": "^1.2.1", - "fs-extra": "^8.1.0", - "node-fetch": "^2.6.0", - "sqlite3": "^4.1.0", - "uuid": "^3.3.3" - }, - "devDependencies": { - "eslint": "^6.3.0", - "eslint-config-airbnb": "^18.0.1", - "eslint-config-airbnb-base": "^14.0.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^1.7.0" - }, - "autoload": { - "App": "./app" - } -} diff --git a/src/server/public/css/main.css b/src/server/public/css/main.css deleted file mode 100644 index a1c5653d7..000000000 --- a/src/server/public/css/main.css +++ /dev/null @@ -1,69 +0,0 @@ -input { - margin-bottom: 1rem; - width: 100%; - padding: 0.5rem; -} - -button, .button { - display: flex; - overflow: hidden; - padding: 12px 12px; - cursor: pointer; - width: 100%; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all 150ms linear; - text-align: center; - white-space: nowrap; - text-decoration: none !important; - text-transform: none; - text-transform: capitalize; - color: #fff !important; - border: 0 none; - border-radius: 4px; - font-size: 13px; - font-weight: 500; - line-height: 1.3; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - justify-content: center; - align-items: center; - flex: 0 0 160px; - box-shadow: 2px 5px 10px #e4e4e4; - color: #FFFFFF; - background: #161616; -} - -#dropzone { - width: 100%; - height: 30vh; - background-color: #ebebeb; - - display: flex; - align-items: center; - justify-content: center; - text-align: center; - - cursor: pointer; -} - -#dropzone p { - font-size: 0.85rem; -} - -#files { - display: none; -} - -.alert { - background-color: #e7a8a6; - padding: 0.8rem; - margin-bottom: 1rem; -} - -td { - word-break: break-all; -} \ No newline at end of file diff --git a/src/server/public/css/vanilla.css b/src/server/public/css/vanilla.css deleted file mode 100644 index 37bc051a2..000000000 --- a/src/server/public/css/vanilla.css +++ /dev/null @@ -1,138 +0,0 @@ -/* Reset */ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} -* { - box-sizing: border-box; -} - - - -/* Variables */ -:root { - --desktop-font-size: 1.3rem/1.5; - --mobile-font-size: 1.1rem/1.4; - --text-color: #2d2d2d; - --link-color: blue; - --primary-color: lightsteelblue; - --secondary-color: aliceblue; - --tertiary-color: whitesmoke; -} - - - - -/* Typography */ -body { - color: var(--text-color); - padding: 3rem; - font: var(--desktop-font-size) -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto, Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol"; -} - -h1,h2,h3,h4,h5,h6,p,blockquote,dl,img,figure { - margin: 2rem 0; -} - -h1,h2,h3,h4,h5,h6 { font-weight: bold; } -h1 { font-size: 200%; } -h2 { font-size: 150%; } -h3 { font-size: 120%; } -h4,h5,h6 { font-size: 100%; } -h5, h6 { text-transform: uppercase; } - -header h1 { border-bottom: 1px solid; } - -p { margin: 2rem 0; } - -a,a:visited { color: var(--link-color); } - -strong, time, b { font-weight: bold; } -em, dfn, i { font-style: italic; } -sub { font-size: 60%; vertical-align: bottom; } -small { font-size: 80%; } - -blockquote, q { - background: var(--secondary-color); - border-left: 10px solid var(--primary-color); - font-family: "Georgia", serif; - padding: 1rem; -} -blockquote p:first-child { margin-top: 0; } -cite { - font-family: "Georgia", serif; - font-style: italic; - font-weight: bold; -} - -kbd,code,samp,pre,var { font-family: monospace; font-weight: bold; } -code, pre { - background: var(--tertiary-color); - padding: 0.5rem 1rem; -} -code pre , pre code { padding: 0; } - - - -/* Elements */ -hr { - background: var(--text-color); - border: 0; - height: 1px; - margin: 4rem 0; -} - -img { max-width: 100%; } - -figure { - border: 1px solid var(--primary-color); - display: inline-block; - padding: 1rem; - width: auto; -} -figure img { margin: 0; } -figure figcaption { font-size: 80%; } - -ul, ol { margin: 2rem 0; padding: 0 0 0 4rem; } - -dl dd { padding-left: 2rem; } - -table { - border: 1px solid var(--primary-color); - border-collapse: collapse; - table-layout: fixed; - width: 100%; -} -table caption { margin: 2rem 0; } -table thead { text-align: center; } -table tbody { text-align: right; } -table tr { border-bottom: 1px solid var(--primary-color); } -table tbody tr:nth-child(even) { background: var(--tertiary-color); } -table th { background: var(--secondary-color); font-weight: bold; } -table th, table td { padding: 1rem; } -table th:not(last-of-type), table td:not(last-of-type) { border-right: 1px solid var(--primary-color); } - - - -/* Mobile Styling */ -@media screen and (max-width: 50rem) { - body { - font: var(--mobile-font-size) -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto, Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol" - } -} \ No newline at end of file diff --git a/src/server/resources/views/import.edge b/src/server/resources/views/import.edge deleted file mode 100644 index 561021a0c..000000000 --- a/src/server/resources/views/import.edge +++ /dev/null @@ -1,18 +0,0 @@ -@layout('layouts.main') - -@section('content') -

Import a Franz account

-

Please login using your Franz account. We will import your services and workspaces.

-
-
-
- -
-
- -
-
- - -
-@endsection diff --git a/src/server/resources/views/index.edge b/src/server/resources/views/index.edge deleted file mode 100644 index 3e0198a09..000000000 --- a/src/server/resources/views/index.edge +++ /dev/null @@ -1,32 +0,0 @@ -@layout('layouts.main') - -@section('content') - -

Internal Ferdi Server

-

You are accessing the local server instance of your Ferdi application. This server is used to enable Ferdi's "Use without an Account" feature.

-

- To use this server in your Ferdi client, open Ferdi's settings and as the - server, enter -

-

- Alternatively, you can import your Franz account. -

- - -@endsection diff --git a/src/server/resources/views/layouts/main.edge b/src/server/resources/views/layouts/main.edge deleted file mode 100644 index 77af30327..000000000 --- a/src/server/resources/views/layouts/main.edge +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - ferdi-internal-server - - {{ style('css/vanilla') }} - {{ style('css/main') }} - - - - @!section('content') - - - diff --git a/src/server/start.js b/src/server/start.js deleted file mode 100644 index 34b2cb5fa..000000000 --- a/src/server/start.js +++ /dev/null @@ -1,41 +0,0 @@ - -/* -|-------------------------------------------------------------------------- -| Http server -|-------------------------------------------------------------------------- -| -| This file bootstraps Adonisjs to start the HTTP server. You are free to -| customize the process of booting the http server. -| -| """ Loading ace commands """ -| At times you may want to load ace commands when starting the HTTP server. -| Same can be done by chaining `loadCommands()` method after -| -| """ Preloading files """ -| Also you can preload files by calling `preLoad('path/to/file')` method. -| Make sure to pass a relative path from the project root. -*/ -const path = require('path'); -const fs = require('fs-extra'); - -process.env.ENV_PATH = path.join(__dirname, 'env.ini'); - -const { Ignitor } = require('@adonisjs/ignitor'); -const fold = require('@adonisjs/fold'); - -module.exports = (dbPath, port) => { - if (!fs.existsSync(dbPath)) { - fs.copySync( - path.join(__dirname, 'database', 'template.sqlite'), - dbPath, - ); - } - - process.env.DB_PATH = dbPath; - process.env.PORT = port; - - new Ignitor(fold) - .appRoot(__dirname) - .fireHttpServer() - .catch(console.error); // eslint-disable-line no-console -}; diff --git a/src/server/start/app.js b/src/server/start/app.js deleted file mode 100644 index a29ca6594..000000000 --- a/src/server/start/app.js +++ /dev/null @@ -1,62 +0,0 @@ - -/* -|-------------------------------------------------------------------------- -| Providers -|-------------------------------------------------------------------------- -| -| Providers are building blocks for your Adonis app. Anytime you install -| a new Adonis specific package, chances are you will register the -| provider here. -| -*/ -const providers = [ - '@adonisjs/framework/providers/AppProvider', - '@adonisjs/bodyparser/providers/BodyParserProvider', - '@adonisjs/cors/providers/CorsProvider', - '@adonisjs/lucid/providers/LucidProvider', - '@adonisjs/drive/providers/DriveProvider', - '@adonisjs/validator/providers/ValidatorProvider', - '@adonisjs/framework/providers/ViewProvider', - '@adonisjs/shield/providers/ShieldProvider', -]; - -/* -|-------------------------------------------------------------------------- -| Ace Providers -|-------------------------------------------------------------------------- -| -| Ace providers are required only when running ace commands. For example -| Providers for migrations, tests etc. -| -*/ -const aceProviders = [ - '@adonisjs/lucid/providers/MigrationsProvider', -]; - -/* -|-------------------------------------------------------------------------- -| Aliases -|-------------------------------------------------------------------------- -| -| Aliases are short unique names for IoC container bindings. You are free -| to create your own aliases. -| -| For example: -| { Route: 'Adonis/Src/Route' } -| -*/ -const aliases = {}; - -/* -|-------------------------------------------------------------------------- -| Commands -|-------------------------------------------------------------------------- -| -| Here you store ace commands for your package -| -*/ -const commands = []; - -module.exports = { - providers, aceProviders, aliases, commands, -}; diff --git a/src/server/start/kernel.js b/src/server/start/kernel.js deleted file mode 100644 index 54fe1f35d..000000000 --- a/src/server/start/kernel.js +++ /dev/null @@ -1,56 +0,0 @@ - -/** @type {import('@adonisjs/framework/src/Server')} */ -const Server = use('Server'); - -/* -|-------------------------------------------------------------------------- -| Global Middleware -|-------------------------------------------------------------------------- -| -| Global middleware are executed on each http request only when the routes -| match. -| -*/ -const globalMiddleware = [ - 'Adonis/Middleware/BodyParser', - 'App/Middleware/ConvertEmptyStringsToNull', -]; - -/* -|-------------------------------------------------------------------------- -| Named Middleware -|-------------------------------------------------------------------------- -| -| Named middleware is key/value object to conditionally add middleware on -| specific routes or group of routes. -| -| // define -| { -| auth: 'Adonis/Middleware/Auth' -| } -| -| // use -| Route.get().middleware('auth') -| -*/ -const namedMiddleware = { -}; - -/* -|-------------------------------------------------------------------------- -| Server Middleware -|-------------------------------------------------------------------------- -| -| Server level middleware are executed even when route for a given URL is -| not registered. Features like `static assets` and `cors` needs better -| control over request lifecycle. -| -*/ -const serverMiddleware = [ - 'Adonis/Middleware/Static', -]; - -Server - .registerGlobal(globalMiddleware) - .registerNamed(namedMiddleware) - .use(serverMiddleware); diff --git a/src/server/start/routes.js b/src/server/start/routes.js deleted file mode 100644 index 333a5ba06..000000000 --- a/src/server/start/routes.js +++ /dev/null @@ -1,74 +0,0 @@ - -/* -|-------------------------------------------------------------------------- -| Routes -|-------------------------------------------------------------------------- -| -*/ - -/** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ -const Route = use('Route'); - -const OnlyAllowFerdi = async ({ request, response }, next) => { - const user = request.header('User-Agent'); - if (!/Ferdi\/\d(\.\d){2}/g.test(user)) { - return response.status(403).redirect('/'); - } - - await next(); - return true; -}; - -// Health: Returning if all systems function correctly -Route.get('health', ({ - response, -}) => response.send({ - api: 'success', - db: 'success', -})).middleware(OnlyAllowFerdi); - -// API is grouped under '/v1/' route -Route.group(() => { - // User authentification - Route.post('auth/signup', 'UserController.signup'); - Route.post('auth/login', 'UserController.login'); - - // User info - Route.get('me', 'UserController.me'); - - // Service info - Route.post('service', 'ServiceController.create'); - Route.put('service/:id', 'ServiceController.edit'); - Route.delete('service/:id', 'ServiceController.delete'); - Route.get('me/services', 'ServiceController.list'); - Route.put('service/reorder', 'ServiceController.reorder'); - Route.get('recipe', 'ServiceController.list'); - Route.post('recipes/update', 'ServiceController.update'); - - // Recipe store - Route.get('recipes', 'RecipeController.list'); - Route.get('recipes/download/:recipe', 'RecipeController.download'); - Route.get('recipes/search', 'RecipeController.search'); - Route.get('recipes/popular', 'StaticController.popularRecipes'); - Route.get('recipes/update', 'StaticController.emptyArray'); - - // Workspaces - Route.put('workspace/:id', 'WorkspaceController.edit'); - Route.delete('workspace/:id', 'WorkspaceController.delete'); - Route.post('workspace', 'WorkspaceController.create'); - Route.get('workspace', 'WorkspaceController.list'); - - // Static responses - Route.get('features', 'StaticController.features'); - Route.get('services', 'StaticController.emptyArray'); - Route.get('news', 'StaticController.emptyArray'); - Route.get('payment/plans', 'StaticController.plans'); - Route.get('announcements/:version', 'StaticController.announcement'); -}).prefix('v1').middleware(OnlyAllowFerdi); - -// Franz account import -Route.post('import', 'UserController.import'); -Route.get('import', ({ view }) => view.render('import')); - -// Index -Route.get('/', ({ view }) => view.render('index')); -- cgit v1.2.3-70-g09d2 From 1d5acd08095eb19b5a554adae82f19ec10051c7a Mon Sep 17 00:00:00 2001 From: vantezzen Date: Fri, 18 Oct 2019 21:44:35 +0200 Subject: Move internal server to submodule --- .gitmodules | 3 +++ src/server | 1 + 2 files changed, 4 insertions(+) create mode 160000 src/server (limited to 'src/server') diff --git a/.gitmodules b/.gitmodules index 5a8d15f8f..f1f9fcec8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "recipes"] path = recipes url = https://github.com/getferdi/recipes +[submodule "src/server"] + path = src/server + url = https://github.com/getferdi/internal-server.git diff --git a/src/server b/src/server new file mode 160000 index 000000000..04bb53426 --- /dev/null +++ b/src/server @@ -0,0 +1 @@ +Subproject commit 04bb53426e3c722a52198e2b9b9ce4283bf123c3 -- cgit v1.2.3-70-g09d2 From 3271a9719df83ac29967f7eb3b2968ea670a81b1 Mon Sep 17 00:00:00 2001 From: vantezzen Date: Sat, 19 Oct 2019 12:32:34 +0200 Subject: Update submodules --- recipes | 2 +- src/server | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/server') diff --git a/recipes b/recipes index 05d4df6d3..c01a693a2 160000 --- a/recipes +++ b/recipes @@ -1 +1 @@ -Subproject commit 05d4df6d3f4a13d20011c14ae633363dcbf16ddc +Subproject commit c01a693a29c84f328b2a353ff16657ce3476edb9 diff --git a/src/server b/src/server index 04bb53426..a2b75e6d3 160000 --- a/src/server +++ b/src/server @@ -1 +1 @@ -Subproject commit 04bb53426e3c722a52198e2b9b9ce4283bf123c3 +Subproject commit a2b75e6d312304770d49254e8cb3f076efce326c -- cgit v1.2.3-70-g09d2