diff --git a/cli/README.md b/cli/README.md index 4c87587..64c200a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -67,7 +67,10 @@ Then open the content of your `my-first-plugin` folder. You should find the gene You can now optionally start a new plugin from a template by appending `--template=[template-name]` to the creation command. -If you don't pass a template, Wasmo will list the available templates: `js`, `ts`, `opa`, `go` and `rust`. +If you don't pass a template, Wasmo will list the available templates. There are listed by product : + - empty template : `js`, `ts`, `opa`, `go` and `rust` + - Otoroshi template : `otoroshi_go`, `otoroshi_rust`, `otoroshi_opa`, `otoroshi_ts`, `otoroshi_js` + - Izanami template : `izanami_js`, `izanami_go`, `izanami_rust`, `izanami_opa`, `izanami_ts` ``` wasmo init --name=my-first-plugin --template=[template-name] --path=[output-directory] diff --git a/docs/documentation/app/builder/collaborate/_page.mdx b/docs/documentation/app/builder/collaborate/_page.mdx new file mode 100644 index 0000000..d994964 --- /dev/null +++ b/docs/documentation/app/builder/collaborate/_page.mdx @@ -0,0 +1,13 @@ + +# Collaborate + +Wasmo 1.2.2 brings a new feature to collaborate inside the product. You can work together on a plugin by sharing it with collaborators. It's easy to share plugins with your entire team. + +To share plugins from the UI: + 1. Click on the desired plugin + 2. Click the 🔗 button + 3. Write administrators and users emails in their respective lists + +
+ +
\ No newline at end of file diff --git a/docs/documentation/app/builder/collaborate/layout.js b/docs/documentation/app/builder/collaborate/layout.js new file mode 100644 index 0000000..9e489dd --- /dev/null +++ b/docs/documentation/app/builder/collaborate/layout.js @@ -0,0 +1,7 @@ +import Page from './page'; + +export const metadata = { + title: 'Builder - UI', +} + +export default Page; \ No newline at end of file diff --git a/docs/documentation/app/builder/collaborate/page.js b/docs/documentation/app/builder/collaborate/page.js new file mode 100644 index 0000000..6a9c69e --- /dev/null +++ b/docs/documentation/app/builder/collaborate/page.js @@ -0,0 +1,24 @@ +"use client" + +import Layout from '@/components/Layout'; +import Page from './_page.mdx'; + +export default function Home() { + + return + + + +} \ No newline at end of file diff --git a/docs/documentation/app/builder/ui/page.js b/docs/documentation/app/builder/ui/page.js index dcb8f0d..d76ccf6 100644 --- a/docs/documentation/app/builder/ui/page.js +++ b/docs/documentation/app/builder/ui/page.js @@ -11,8 +11,8 @@ export default function Home() { href: '/builder/ui' }} previous={{ - href: "/builder/plugin-structure", - title: "Plugin Structure" + href: "/builder/collaborate", + title: "Collaborate" }} next={{ href: "/cli/getting-started", diff --git a/docs/documentation/app/cli/getting-started/_page.mdx b/docs/documentation/app/cli/getting-started/_page.mdx index 643213a..6851926 100644 --- a/docs/documentation/app/cli/getting-started/_page.mdx +++ b/docs/documentation/app/cli/getting-started/_page.mdx @@ -43,7 +43,10 @@ Then open the content of your `my-first-plugin` folder. You should find the gene You can now optionally start a new plugin from a template by appending `--template=[template-name]` to the creation command. -If you don't pass a template, Wasmo will list the available templates: `js`, `ts`, `opa`, `go` and `rust`. +If you don't pass a template, Wasmo will list the available templates. There are listed by product : + - empty template : `js`, `ts`, `opa`, `go` and `rust` + - Otoroshi template : `otoroshi_go`, `otoroshi_rust`, `otoroshi_opa`, `otoroshi_ts`, `otoroshi_js` + - Izanami template : `izanami_js`, `izanami_go`, `izanami_rust`, `izanami_opa`, `izanami_ts` ``` wasmo init --name=my-first-plugin --template=[template-name] --path=[output-directory] @@ -61,7 +64,11 @@ You have two ways to build your plugin: [wasmoserver]: https://github.com/MAIF/wasmo -Assuming we want to build our `my-first-plugin` locally. Enter `wasmo build --host=OneShotDocker --path=my-first-plugin` to start the build. +Assuming we want to build our `my-first-plugin` locally. Enter the following command to start the build. + +``` +wasmo build --host=OneShotDocker --path=my-first-plugin +``` Let's explain these 3 parameters: - the `path` parameter is explicitly used to indicate the plugin to build diff --git a/docs/documentation/app/faq/_page.mdx b/docs/documentation/app/faq/_page.mdx new file mode 100644 index 0000000..5160f5c --- /dev/null +++ b/docs/documentation/app/faq/_page.mdx @@ -0,0 +1,109 @@ +import Badge from '../../components/Badge' +import Badges from '../../components/Badges' +import FAQButton from '../../components/FAQButton' + +# FAQ + + + + + + + + + + +### What is the fastest way to use Wasmo? + +``` +$ cargo install wasmo +or +$ brew tap maif/wasmo +$ brew install wasmo +``` + +### How to create an Otoroshi-compatible plugin using Docker? + + +Initialize the plugin with corresponding Otoroshi template and Javascript language +``` +wasmo init --template=otoroshi_js --name=foo +``` + +Build plugin from folder and Docker +``` +wasmo build --host=OneShotDocker --path=. +``` + +### How can I create a new development version of my plugin? + + +Rust plugin + +``` Cargo.toml +[package] +name = "foo" +version = "1.0.2" + +... +``` + +JS/TS/Open Policy Agent plugin + +``` package.json +{ + "name": "foo", + "version": "1.0.2", + ... +} +``` + +Go plugin + +``` go.mod +module foo/1.0.2 + +... +``` + +
+ + +
+ ### Can I download the generated Wasm from the UI? + + Once you have built a dev or release version of your plugin using the Hammer or Rocker buttons (available at the top right of the screen), + you can click on each version under the 'Releases' section on the left side of the screen. + + ### Can I determine who built each version and at what time? + + Each plugin has a **config** file under the **configuration** section with the following information : + - type: language used to develop the plugin. + - users: list of users allowed to edit and view the plugin. + - admins: list of admins allowed to edit, view and share the plugin. + - filename: name of the plugin. + - pluginId: unique ID of the plugin. + - template: original template, selected at plugin creation. + - **versions: list of built versions, with name, creator and date of generation**. + - last_hash: hash used by the backend to check if changes has been made between last version. +
+
+ +### How can I collaborate with my team? + +Since version 1.22, Wasmo allows users to share plugins with two levels of rights: +- `users`: Can edit and view plugin. +- `admins`: Can edit, view and share plugin. + +You can find more information about sharing by reading this [article](/wasmo/builder/collaborate) + +### Our team have a CI/CD process and wants to automate the building of our plugins. + +Since version 1.x, Wasmo includes a command line interface to create, edit and build plugins. + +You can find more [information](/wasmo/cli/getting-started) about the CLI and the Github [repository](https://github.com/MAIF/wasmo/tree/main/cli) \ No newline at end of file diff --git a/docs/documentation/app/faq/layout.js b/docs/documentation/app/faq/layout.js new file mode 100644 index 0000000..4962903 --- /dev/null +++ b/docs/documentation/app/faq/layout.js @@ -0,0 +1,7 @@ +import Page from './page'; + +export const metadata = { + title: 'FAQ', +} + +export default Page; \ No newline at end of file diff --git a/docs/documentation/app/faq/page.js b/docs/documentation/app/faq/page.js new file mode 100644 index 0000000..2ec87a6 --- /dev/null +++ b/docs/documentation/app/faq/page.js @@ -0,0 +1,20 @@ +"use client" + +import Layout from '@/components/Layout'; +import Page from './_page.mdx'; + +export default function Home() { + + return + + + +} \ No newline at end of file diff --git a/docs/documentation/app/globals.css b/docs/documentation/app/globals.css index a8aa6a2..6e2ff3e 100644 --- a/docs/documentation/app/globals.css +++ b/docs/documentation/app/globals.css @@ -16,4 +16,38 @@ overflow: auto; max-height: calc(500px - 71px); margin-bottom: 24px; +} + +.sidebar-group::before { + position: absolute; + bottom: -0.1rem; + top: -0.1rem; + left: -0.5rem; + width: 10px; + --tw-border-opacity: 1; + border-right-width: 2px; + border-color: rgb(226 232 240 / var(--tw-border-opacity)); + opacity: 1; + content: ""; +} + +.sidebar-group-selected::before { + border-color: rgb(126 34 206); +} + +h2, h3 { + position: relative; +} + +.anchor-link { + color: #666; + opacity: 0; + position: absolute; + transform: translate(-1em, -2px); + width: 1em; +} + +h3:hover .anchor-link, +h2:hover .anchor-link { + opacity: 1; } \ No newline at end of file diff --git a/docs/documentation/components/Badge.js b/docs/documentation/components/Badge.js new file mode 100644 index 0000000..c8eec99 --- /dev/null +++ b/docs/documentation/components/Badge.js @@ -0,0 +1,7 @@ +export default function Badge({ value, raw, ...props }) { + return
+ + {!raw ? `<${value}>` : value} + +
+} \ No newline at end of file diff --git a/docs/documentation/components/Badges.js b/docs/documentation/components/Badges.js index 88e92f5..23682fa 100644 --- a/docs/documentation/components/Badges.js +++ b/docs/documentation/components/Badges.js @@ -1,5 +1,5 @@ -export default function Badges({ values, raw }) { - return
+export default function Badges({ values, raw, ...props }) { + return
{values.map(value => {!raw ? `<${value}>` : value} )} diff --git a/docs/documentation/components/FAQButton.js b/docs/documentation/components/FAQButton.js new file mode 100644 index 0000000..eb64260 --- /dev/null +++ b/docs/documentation/components/FAQButton.js @@ -0,0 +1,21 @@ +export default function FAQButton({ title }) { + return +} \ No newline at end of file diff --git a/docs/documentation/components/Layout.js b/docs/documentation/components/Layout.js index 080cd37..1e9cd54 100644 --- a/docs/documentation/components/Layout.js +++ b/docs/documentation/components/Layout.js @@ -49,7 +49,7 @@ function Layout({ children, next, metadata, previous }) { table: Table, th: props => {props.children}, thead: props => {props.children}, - h3: props =>

{props.children}

, + h3: Heading.H3, h4: props =>

{props.children}

}}> {children} diff --git a/docs/documentation/components/Searchbar.js b/docs/documentation/components/Searchbar.js index 53f47b2..ab93494 100644 --- a/docs/documentation/components/Searchbar.js +++ b/docs/documentation/components/Searchbar.js @@ -70,7 +70,9 @@ export default function Searchbar({ handleOpen, open }) { paddingTop: open ? 20 : 6 }}> - {!open &&
+ {!open &&
  • Overview
  • +
  • + + FAQ +
    New
    +
    +
  • {Object.entries(LINKS).map(([group, children]) => { return
  • @@ -65,15 +95,38 @@ export function Sidebar({ metadata }) { -
      +
        {children.map(child => { const href = `/wasmo/${slugify(group)}/${slugify(child)}`; - return
      • + return
      • {child} + + {NEWS.includes(child) &&
        New
        }
      • })} diff --git a/docs/documentation/components/mdx/Heading.js b/docs/documentation/components/mdx/Heading.js index 26384bd..5d2741b 100644 --- a/docs/documentation/components/mdx/Heading.js +++ b/docs/documentation/components/mdx/Heading.js @@ -1,6 +1,38 @@ +function getAnchor(children) { + const text = Array.isArray(children) ? children[0] : children + return ("" + text) + .toLowerCase() + .replace(/[^a-z0-9 ]/g, '') + .replace(/[ ]/g, '-'); +} + export const Heading = { H1: ({ children }) =>

        {children}

        , - H2: props => { - return

        {props.children}

        + // H2: props => { + // return

        {props.children}

        + // }, + H2: ({ children }) => { + const anchor = getAnchor(children); + const link = `#${anchor}`; + return ( +

        + + § + + {children} +

        + ); }, + H3: ({ children }) => { + const anchor = getAnchor(children); + const link = `#${anchor}`; + return ( +

        + + § + + {children} +

        + ); + } }; \ No newline at end of file diff --git a/docs/documentation/components/mdx/Pre.js b/docs/documentation/components/mdx/Pre.js index 96f6cde..ec73f26 100644 --- a/docs/documentation/components/mdx/Pre.js +++ b/docs/documentation/components/mdx/Pre.js @@ -31,15 +31,16 @@ export const Pre = props => { {!['javascript', 'go', 'rust', 'js'].includes(language) && {language}}
  • } -
    { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(codeString); setCopied(true) } - }} - style={{ - border: '1px solid #fff' }}> diff --git a/docs/documentation/public/collaborate.png b/docs/documentation/public/collaborate.png new file mode 100644 index 0000000..4ef7575 Binary files /dev/null and b/docs/documentation/public/collaborate.png differ diff --git a/docs/documentation/public/release.png b/docs/documentation/public/release.png new file mode 100644 index 0000000..36f2f70 Binary files /dev/null and b/docs/documentation/public/release.png differ diff --git a/server/datastores/api.js b/server/datastores/api.js index 566ff44..1551b29 100755 --- a/server/datastores/api.js +++ b/server/datastores/api.js @@ -4,6 +4,16 @@ module.exports = class Datastore { * Initialize the datastore */ async initialize() { Promise.resolve() } + /** + * List of created plugins, whole database + */ + getPlugins() { Promise.resolve() } + /** + * Check if user is in the users or admins list of specific plugin + * @param {string} email + * @param {string} pluginId + */ + hasRights(email, pluginId) { Promise.resolve() } /** * Get current user */ @@ -13,6 +23,26 @@ module.exports = class Datastore { * @returns {Object[]} */ getUserPlugins(email) { return Promise.resolve() } + /** + * Add plugin to the list of plugins + * @param {string} email + * @param {any} plugin + * @returns + */ + addPluginToList = (email, plugin) => Promise.resolve() + /** + * Update plugin informations like users, admins list + * @param {string} pluginId + * @param {any} content + * @returns + */ + updatePluginList = (pluginId, content) => Promise.resolve() + /** + * Remove plugin from list of plugins + * @param {string} pluginId + * @returns + */ + removePluginFromList = (pluginId) => Promise.resolve() /** * register a new user if not present * @param {string} email @@ -32,8 +62,11 @@ module.exports = class Datastore { /** * Save wasm file * @param {string} wasmFolder + * @param {string} pluginId + * @param {string} newHash + * @param {string} generateWasmName */ - putWasmFileToS3(wasmFolder) { return Promise.resolve() } + putWasmFileToS3(email, pluginId, newHash, generateWasmName) { return Promise.resolve() } /** * Save logs file @@ -44,12 +77,11 @@ module.exports = class Datastore { /** * Save plugin information after build process - * @param {string} email * @param {string} pluginId * @param {string} newHash * @param {string} generateWasmName */ - putWasmInformationsToS3(email, pluginId, newHash, generateWasmName) { return Promise.resolve() } + pushNewPluginVersion(email, pluginId, newHash, generateWasmName) { return Promise.resolve() } /** * Fetch wasm from datastore @@ -63,24 +95,26 @@ module.exports = class Datastore { * @param {JSON} runOptions */ runWasm(wasmId, runOptions) { return Promise.resolve() } - /** * Check the presence of a specific wasm in database * @param {string} wasmId * @param {boolean} release */ - isWasmExists(wasmId, release) { - return Promise.resolve() - } - + isWasmExists(wasmId, release) { return Promise.resolve() } /** * Fetch plugin sources * @param {string} pluginId * @returns sources as buffer */ - getSources = pluginId => { - return Promise.resolve() - } + getSources = pluginId => { return Promise.resolve() } + + /** + * Get log files of specific plugin at add them to files + * @param {string} pluginId + * @param {any} files + * @returns + */ + getConfigurationsFile (pluginId, files) { return Promise.resolve() } /** * Fetch configuration file @@ -92,12 +126,36 @@ module.exports = class Datastore { } /** - * Delete specific plugin + * Remove all wasm version of specific plugins + * @param {string[]} versions + * @returns + */ + removeBinaries = versions => { + return Promise.resolve() + } + + /** + * Remove object from S3 + * @param {string} key + * @returns + */ + deleteObject = key => { return Promise.resolve() } + + /** + * Delete specific plugin and all assets * @param {string} email * @param {string} pluginId */ deletePlugin = (email, pluginId) => Promise.resolve() + /** + * Update plugin informations + * @param {string} id + * @param {any} body - plugin content + * @returns + */ + updatePluginInformations = (pluginId, body) => Promise.resolve() + /** * Update plugin content * @param {string} id @@ -111,6 +169,15 @@ module.exports = class Datastore { * @param {JSON} metadata */ createEmptyPlugin = (email, metadata, isGithub) => Promise.resolve() + /** + * Update plugin field + * @param {string} email + * @param {string} pluginId + * @param {string} field + * @param {any} value + * @returns + */ + patchPlugin = (email, pluginId, field, value) => Promise.resolve() /** * Edit the plugin name @@ -137,12 +204,12 @@ module.exports = class Datastore { getPluginUsers = (email, pluginId) => Promise.resolve() /** - * Get plugin of user - * @param {string} owner + * Get plugin (check if user can access it) + * @param {string} email * @param {string} pluginId * @returns */ - getPlugin = (owner, pluginId) => Promise.resolve() + getPlugin = (email, pluginId) => Promise.resolve() /** * Check if user can share plugin @@ -180,14 +247,19 @@ module.exports = class Datastore { * Check if received link is valid and add plugin to the current user * @param {string} pluginId */ - acceptInvitation = (userId, ownerId, pluginId) => Promise.resolve() + acceptInvitation = (userId, pluginId) => Promise.resolve() /** * Get invitation informations - * @param {string} userId - * @param {string} ownerId + * @param {string} email * @param {string} pluginId - * @returns */ - getInvitation = (userId, ownerId, pluginId) => Promise.resolve() + getInvitation = (email, pluginId) => Promise.resolve() + + /** + * Check if user are allowed to share plugin to other users + * @param {string} email + * @param {string} pluginId + */ + canSharePlugin = (email, pluginId) => Promise.resolve() }; \ No newline at end of file diff --git a/server/datastores/postgres.js b/server/datastores/postgres.js index 5e5598d..50f3250 100755 --- a/server/datastores/postgres.js +++ b/server/datastores/postgres.js @@ -10,10 +10,6 @@ const { Pool } = require('pg'); const logger = require("../logger"); const { isAString } = require('../utils'); -/** - * Class representing PG. - * @extends Datastore - */ module.exports = class PgDatastore extends Datastore { /** @type {S3Datastore} */ #sourcesDatastore = undefined; @@ -51,35 +47,29 @@ module.exports = class PgDatastore extends Datastore { const client = await this.#pool.connect(); await Promise.all([ - client.query("CREATE TABLE IF NOT EXISTS users(id SERIAL, email VARCHAR, content JSONB)"), + client.query("CREATE TABLE IF NOT EXISTS plugins(id VARCHAR(200), content JSONB)"), client.query("CREATE TABLE IF NOT EXISTS jobs(id SERIAL, plugin_id VARCHAR UNIQUE, created_at TIMESTAMP default current_timestamp)"), ]); client.release() } - getUser = (email) => { + getPlugins = () => { return this.#pool.connect() .then(async client => { - const res = await client.query("SELECT content from users WHERE email = $1", [email]) + const res = await client.query("SELECT content FROM plugins", []) client.release(); - return res.rowCount > 0 ? res.rows[0].content : {}; + return res.rowCount > 0 ? res.rows.map(r => r.content) : [] }) } - getUserPlugins = (email) => { - return this.getUser(email) - .then(data => data.plugins || []) - .then(plugins => { - return Promise.all(plugins.map(plugin => { - if (plugin.owner) { - return this.#getPlugin(plugin.owner, plugin.pluginId) - .then(res => ({ ...res, owner: plugin.owner })) - } else { - return Promise.resolve(plugin) - } - }) - ) + getUserPlugins = async email => { + return this.#pool.connect() + .then(async client => { + const res = await client.query("SELECT content FROM plugins WHERE content->'admins' ? $1::text OR content->'users' ? $1::text", [email]) + client.release(); + + return res.rowCount > 0 ? res.rows.map(r => r.content) : [] }) } @@ -104,28 +94,6 @@ module.exports = class PgDatastore extends Datastore { }) } - updateUser = (email, content) => { - return this.#pool.connect() - .then(client => { - return client.query("UPDATE users SET content = $1::jsonb WHERE email = $2", [content, email]) - .then(async res => { - if (res.rowCount === 0) { - await client.query("INSERT INTO users (content, email) VALUES($1::jsonb, $2)", [content, email]) - } - - client.release() - }); - }) - .then(() => ({ status: 200 })) - .catch(err => { - console.log('update user error', err) - return { - error: 400, - status: 'something bad happened' - } - }); - } - putWasmFileToS3 = (wasmFolder) => { return this.#sourcesDatastore.putWasmFileToS3(wasmFolder); } @@ -134,51 +102,39 @@ module.exports = class PgDatastore extends Datastore { return this.#sourcesDatastore.putBuildLogsToS3(logId, logsFolder); } - putWasmInformationsToS3 = (email, pluginId, newHash, generateWasmName) => { - return this.getUser(email) - .then(data => { - const plugin = data.plugins.map(plugin => plugin.pluginId === pluginId) - - if (plugin.owner) - return this.putWasmInformationsToS3(plugin.owner, pluginId, newHash, generateWasmName) + pushNewPluginVersion = (email, pluginId, newHash, generateWasmName) => { + return this.getPlugin(email, pluginId) + .then(plugin => { + let versions = plugin.versions || []; - const newPlugins = data.plugins.map(plugin => { - if (plugin.pluginId !== pluginId) { - return plugin; - } - let versions = plugin.versions || []; + // convert legacy array + if (versions.length > 0 && isAString(versions[0])) { + versions = versions.map(name => ({ name })) + } - // convert legacy array - if (versions.length > 0 && isAString(versions[0])) { - versions = versions.map(name => ({ name })) + const index = versions.findIndex(item => item.name === generateWasmName); + if (index === -1) + versions.push({ + name: generateWasmName, + updated_at: Date.now(), + creator: email + }) + else { + versions[index] = { + ...versions[index], + updated_at: Date.now(), + creator: email } + } - const index = versions.findIndex(item => item.name === generateWasmName); - if (index === -1) - versions.push({ - name: generateWasmName, - updated_at: Date.now(), - creator: email - }) - else { - versions[index] = { - ...versions[index], - updated_at: Date.now(), - creator: email - } - } + const patchPlugin = { + ...plugin, + last_hash: newHash, + wasm: generateWasmName, + versions + } - return { - ...plugin, - last_hash: newHash, - wasm: generateWasmName, - versions - } - }); - return this.updateUser(email, { - ...data, - plugins: newPlugins - }) + return this.updatePluginList(pluginId, patchPlugin) }) } @@ -198,20 +154,39 @@ module.exports = class PgDatastore extends Datastore { return this.#sourcesDatastore.getSources(pluginId); } - #getPlugin = (email, pluginId) => { - return this.#pool.connect() - .then(client => { - return client.query("SELECT content FROM users WHERE email = $1", [email]) - .then(res => { - client.release(); + hasRights = (email, pluginId) => { + if (email === "*") + return Promise.resolve() - return res.rowCount > 0 ? (res.rows[0].content.plugins || [])?.find(plugin => plugin.pluginId === pluginId) : {} - }) + return this.#pool.connect() + .then(client => client.query("SELECT * FROM plugins WHERE id = $1::text AND (content->'admins' ? $2::text OR content->'users' ? $2::text)", [pluginId, email]) + .then(res => { + client.release() + return res.rowCount === 1 + })) + .then(authorized => { + if (!authorized) + throw 'Not authorized' }) } + getPlugin = async (email, pluginId) => { + try { + await this.hasRights(email, pluginId) + } catch (_err) { + return {} + } + + return this.#pool.connect() + .then(client => client.query("SELECT * FROM plugins WHERE id = $1::text", [pluginId]) + .then(res => { + client.release() + return res.rowCount === 1 ? res.rows[0].content : {} + })) + } + getConfigurations = async (email, pluginId) => { - const plugin = await this.#getPlugin(email, pluginId); + const plugin = await this.getPlugin(email, pluginId); if (plugin.owner) return this.getConfigurations(plugin.owner, pluginId) @@ -228,60 +203,34 @@ module.exports = class PgDatastore extends Datastore { return this.#sourcesDatastore.getConfigurationsFile(plugin.pluginId, files); } - #deleteRootPlugin = async (plugin, pluginId) => { - const { admins, users } = plugin; - - await Promise.all([ - ...(admins || []), - ...(users || []) - ].map(user => this.#removePluginFromUser(user, pluginId))) - - return this.deletePlugin(plugin.owner, pluginId) - } - deletePlugin = (email, pluginId) => { - return new Promise(resolve => this.getUser(email) - .then(data => { - if (Object.keys(data).length > 0) { - const plugin = data.plugins.find(f => f.pluginId === pluginId) - - this.updateUser(email, { - ...data, - plugins: data.plugins.filter(f => f.pluginId !== pluginId) - }) - .then(() => { - if (plugin.owner || plugin.users || plugin.admins) { - this.#deleteRootPlugin(plugin, pluginId) - .then(resolve) - } else { - const pluginHash = (plugin || {}).last_hash; - - Promise.all([ - this.#sourcesDatastore.deleteObject(`${pluginHash}.zip`), - this.#sourcesDatastore.deleteObject(`${pluginHash}-logs.zip`), - this.#sourcesDatastore.deleteObject(`${pluginId}.zip`), - this.#sourcesDatastore.deleteObject(`${pluginId}-logs.zip`), - this.#sourcesDatastore.deleteObject(`${pluginId}.zip`), - this.#sourcesDatastore.deleteObject(`${pluginId}-logs.zip`), - this.#sourcesDatastore.removeBinaries((plugin.versions || []).map(r => r.name)) - ]) - .then(() => resolve({ status: 204, body: null })) - .catch(err => { - resolve({ - status: 400, - body: { - error: err.message - } - }) - }) + return new Promise(resolve => this.getPlugin(email, pluginId) + .then(plugin => { + Promise.all([ + this.#sourcesDatastore.deleteObject(`${pluginId}.zip`), + this.#sourcesDatastore.deleteObject(`${pluginId}-logs.zip`), + this.#sourcesDatastore.removeBinaries((plugin.versions || []).map(r => r.name)), + this.#sourcesDatastore.deleteObject(`${pluginId}.json`), + this.removePluginFromList(pluginId) + ]) + .then(() => resolve({ status: 204, body: null })) + .catch(err => { + resolve({ + status: 400, + body: { + error: err.message } }) - } else { - resolve({ - status: 204, body: null }) - } })) + .catch(_ => { + resolve({ + status: 400, + body: { + error: "something bad happened" + } + }) + }) } updatePlugin = (id, body) => { @@ -290,135 +239,118 @@ module.exports = class PgDatastore extends Datastore { createEmptyPlugin = (email, metadata, isGithub) => { return new Promise(resolve => { - this.createUserIfNotExists(email) - .then(() => { - this.getUser(email) - .then(data => { - const pluginId = crypto.randomUUID() - const plugins = [ - ...(data.plugins || []), - isGithub ? { - filename: metadata.repo, - owner: metadata.owner, - ref: metadata.ref, - type: 'github', - pluginId: pluginId, - private: metadata.private - } : { - filename: metadata.name.replace(/ /g, '-'), - type: metadata.type, - pluginId: pluginId, - template: metadata.template - } - ] - - return this.#pool.connect() - .then(client => { - return client.query("UPDATE users SET content = $1::jsonb WHERE email = $2", [{ plugins }, email]) - .then(() => client.release()) - }) - .then(() => { - resolve({ - status: 201, - body: { - plugins - } - }) - }) - .catch(err => { - resolve({ - status: 400, - body: { - error: err.message - } - }) - }) + const pluginId = crypto.randomUUID() + const newPlugin = isGithub ? { + filename: metadata.repo, + owner: metadata.owner, + ref: metadata.ref, + type: 'github', + pluginId: pluginId, + private: metadata.private + } : { + filename: metadata.name.replace(/ /g, '-'), + type: metadata.type, + pluginId: pluginId, + template: metadata.template + } + + console.log('generate pluginID', pluginId, newPlugin.pluginId) + + return this.#pool.connect() + .then(client => { + return client.query("INSERT INTO plugins(id, content) VALUES($1, $2::jsonb)", [newPlugin.pluginId, JSON.stringify({ + ...newPlugin, + admins: [email], + users: [] + })]) + .then(() => { + client.release() + return this.getUserPlugins(email) }) }) + .then(plugins => { + resolve({ + status: 201, + body: { + plugins + } + }) + }) .catch(err => { + console.log(err) resolve({ - status: 400, + status: err.$metadata.httpStatusCode, body: { - error: err.message + error: err.Code, + status: err.$metadata.httpStatusCode } }) }) }) } - patchPlugin = (email, pluginId, field, value) => { - return this.getUser(email) - .then(data => { - const plugin = (data.plugins || []).find(plugin => plugin.pluginId === pluginId) - - if (plugin.owner) - return this.patchPlugin(plugin.owner, pluginId, field, value) - else - return this.updateUser(email, { - ...data, - plugins: (data.plugins || []).map(plugin => { - if (plugin.pluginId === pluginId) { - return { - ...plugin, - [field]: value - } - } else { - return plugin - } - }) - }) - }) - } + patchPlugin = async (email, pluginId, field, value) => { + const plugin = await this.getPlugin(email, pluginId) - patchPluginName = (email, pluginId, newName) => { - return this.patchPlugin(email, pluginId, 'filename', value) + return this.updatePluginList(pluginId, { + ...plugin, + [field]: value + }) } - #removePluginFromUser = async (email, pluginId) => { - const user = await this.getUser(email) - - return this.updateUser(email, { - ...user, - plugins: (user.plugins || []).filter(plugin => plugin.pluginId !== pluginId) - }) + patchPluginName = (email, pluginId, value) => { + return this.patchPlugin(email, pluginId, 'filename', value) } patchPluginUsers = async (email, pluginId, newUsers, newAdmins) => { - let currentUser = email; + const plugin = await this.getPlugin(email, pluginId) - const user = await this.getUser(currentUser); - - let plugin = user.plugins.find(plugin => plugin.pluginId === pluginId) + if (plugin) { + this.updatePluginList(pluginId, { + ...plugin, + users: newUsers, + admins: newAdmins + }) + return { + status: 204, + data: null + } + } else { + return { + status: 404, + data: { + error: 'something bad happened' + } + } + } + } - // if plugin is not owned by the user, we need to edit the root plugin - if (plugin.owner) { - currentUser = plugin.owner; + addPluginToList = async (email, plugin) => { + return this.#pool.connect() + .then(client => { + return client.query("INSERT INTO plugins(id, content) VALUES($1, $2::jsonb)", [plugin.pluginId, JSON.stringify({ + pluginId: plugin.pluginId, + admins: [email], + users: [] + })]) + .then(() => client.release()) + }) + } - const owner = await this.getUser(plugin.owner) - plugin = owner.plugins.find(plugin => plugin.pluginId === pluginId) - } + updatePluginList = async (pluginId, content) => { + return this.#pool.connect() + .then(client => { + return client.query("UPDATE plugins SET content = $1::jsonb WHERE id = $2", [JSON.stringify(content), pluginId]) + .then(() => client.release()) + }) + } - const removedUsers = (plugin.users || []).filter(user => !newUsers.includes(user) && !newAdmins.includes(user)) - const removedAdmins = (plugin.admins || []).filter(admin => !newAdmins.includes(admin) && !newUsers.includes(admin)) - - await Promise.all(removedUsers.map(user => this.#removePluginFromUser(user, pluginId))) - await Promise.all(removedAdmins.map(admin => this.#removePluginFromUser(admin, pluginId))) - - return this.getUser(currentUser) - .then(data => this.updateUser(currentUser, { - ...data, - plugins: (data.plugins || []).map(plugin => { - if (plugin.pluginId === pluginId) { - return { - ...plugin, - users: newUsers, - admins: newAdmins - } - } else { - return plugin - } - }) - })) + removePluginFromList = async (pluginId) => { + return this.#pool.connect() + .then(client => { + return client.query("DELETE FROM plugins WHERE id = $1", [pluginId]) + .then(() => client.release()) + }) } isJobRunning = pluginId => { @@ -473,79 +405,26 @@ module.exports = class PgDatastore extends Datastore { }); } - acceptInvitation = async (userEmail, ownerId, pluginId) => { - try { - const ownerPlugins = await this.getUserPlugins(ownerId) - const user = await this.getUser(userEmail) - - const ownerPlugin = ownerPlugins.find(plugin => plugin.pluginId === pluginId) - - if (ownerPlugin && - ((ownerPlugin.users || []).includes(userEmail) || - (ownerPlugin.admins || []).includes(userEmail))) { - - const newPlugin = { - pluginId: ownerPlugin.pluginId, - owner: ownerId - } - - return this.updateUser(userEmail, { - ...user, - plugins: [ - ...(user.plugins || []).filter(plugin => plugin.pluginId !== pluginId), - newPlugin - ] - }) - .then(() => true) - } else { - return false - } - } catch (err) { - console.log(err) - return false - } - } + getInvitation = (email, pluginId) => this.getPlugin(email, pluginId) canSharePlugin = async (email, pluginId) => { - const user = await this.getUser(email); - - const plugin = user.plugins.find(plugin => plugin.pluginId === pluginId); - - if (plugin?.owner) { - const owner = await this.getUser(plugin.owner) - - const rootPlugin = owner.plugins.find(plugin => plugin.pluginId === pluginId); - - if (rootPlugin) { - return (rootPlugin.admins || []).includes(email) - } - - return false - } - - return true - + return this.#pool.connect() + .then(client => client.query("SELECT * FROM plugins WHERE id = $1::text AND content->'admins' ? $2::text", [pluginId, email]) + .then(res => { + client.release() + return res.rowCount > 0 + })) } - getPluginUsers = async (email, pluginId) => { - const user = await this.getUser(email); - - const plugin = user.plugins.find(plugin => plugin.pluginId === pluginId); - - if (plugin.owner) { - const owner = await this.getUser(plugin.owner) - - const rootPlugin = owner.plugins.find(plugin => plugin.pluginId === pluginId); - - return { - admins: rootPlugin.admins, - users: rootPlugin.users - } - } - - return { - admins: plugin.admins, - users: plugin.users - } - } -}; \ No newline at end of file + getPluginUsers = (email, pluginId) => { + return this.#pool.connect() + .then(client => client.query(`SELECT content->'admins' as admins, content->'users' as users + FROM plugins + WHERE id = $1::text AND content->'admins' ? $2::text`, [pluginId, email]) + .then(res => { + client.release() + return res.rowCount > 0 ? res.rows[0] : { admins: [], users: [] } + })) + .then(data => ({ status: 200, data })) + } +} \ No newline at end of file diff --git a/server/datastores/s3.js b/server/datastores/s3.js index 8c98ba2..29dfc09 100755 --- a/server/datastores/s3.js +++ b/server/datastores/s3.js @@ -5,8 +5,7 @@ const { GetObjectCommand, S3Client, HeadBucketCommand, CreateBucketCommand, - DeleteObjectCommand, - DeleteBucketCommand + DeleteObjectCommand } = require("@aws-sdk/client-s3"); const { fromUtf8 } = require("@aws-sdk/util-utf8-node"); @@ -15,19 +14,14 @@ const dns = require('dns'); const url = require('url'); const fs = require('fs-extra'); -const { format, isAString } = require('../utils'); +const { isAString } = require('../utils'); const Datastore = require('./api'); const { ENV, STORAGE } = require("../configuration"); const logger = require("../logger"); -const consumers = require('node:stream/consumers'); const AdmZip = require("adm-zip"); const { Console } = require('console'); const CustomStream = require('./CustomStream'); -/** - * Class representing S3. - * @extends Datastore - */ module.exports = class S3Datastore extends Datastore { #state = { instance: undefined, @@ -101,90 +95,13 @@ module.exports = class S3Datastore extends Datastore { return this.#createBucketIfMissing(); } - getUser = (email) => { - const { instance, Bucket } = this.#state; - - return new Promise(resolve => { - instance.send(new GetObjectCommand({ - Bucket, - Key: `${format(email)}.json` - })) - .then(data => { - try { - if (data && data.Body) { - consumers.json(data.Body) - .then(resolve) - } - else - resolve({}) - } catch (_err) { - resolve({}) - } - }) - .catch(_err => { - resolve({}) - }) - }) - } - - getPlugin = async (owner, pluginId) => { - const user = await this.getUser(owner); - - return user.plugins.find(plugin => plugin.pluginId === pluginId); - } - - getUserPlugins = (email) => { - return this.getUser(email) - .then(data => data.plugins || []) - .then(plugins => Promise.all(plugins.map(plugin => { - if (plugin.owner) { - return this.getPlugin(plugin.owner, plugin.pluginId) - .then(res => ({ ...res, owner: plugin.owner })) - } else { - return Promise.resolve(plugin) - } - }))) - } - - #addUser = async (email) => { - const { instance, Bucket } = this.#state; - - const users = await this.getUsers(); - - await instance.send(new PutObjectCommand({ - Bucket, - Key: 'users.json', - Body: fromUtf8(JSON.stringify([ - ...users, - email - ])), - ContentType: 'application/json', - })) - } - - createUserIfNotExists = async (email) => { - const { instance, Bucket } = this.#state;; - - // console.log("attempt to create user : " + email) - try { - const res = await instance.send(new HeadObjectCommand({ - Bucket, - Key: `${format(email)}.json` - })) - // console.log('user file has been retrieved : ' + email); - } catch (err) { - // console.log('user file not found : ' + email); - await this.#addUser(format(email)) - } - } - - getUsers = async () => { + getPlugins = async () => { const { instance, Bucket } = this.#state; try { const rawData = await instance.send(new GetObjectCommand({ Bucket, - Key: 'users.json' + Key: 'plugins.json' })) return new fetch.Response(rawData.Body).json() } catch (err) { @@ -196,28 +113,44 @@ module.exports = class S3Datastore extends Datastore { } } - updateUser = (email, content) => { + hasRights = (email, pluginId) => { + return this.getPlugins() + .then(plugins => { + + if (email === "*") + return + + const plugin = plugins.find(plugin => plugin.pluginId === pluginId) + + const users = plugin?.users || []; + const admins = plugin?.admins || []; + + if (users.includes(email) || admins.includes(email)) { + return plugin + } + + // TODO - better error handling + throw 'Not authorized' + }) + } + + getPlugin = async (email, pluginId) => { const { instance, Bucket } = this.#state; - const jsonProfile = format(email); + await this.hasRights(email, pluginId) - return new Promise(resolve => { - instance.send(new PutObjectCommand({ + try { + const rawData = await instance.send(new GetObjectCommand({ Bucket, - Key: `${jsonProfile}.json`, - Body: fromUtf8(JSON.stringify(content)), - ContentType: 'application/json' + Key: `${pluginId}.json` })) - .then(_ => resolve({ - status: 200 - })) - .catch(err => { - resolve({ - error: err.Code, - status: err.$metadata.httpStatusCode - }) - }) - }) + const res = await new fetch.Response(rawData.Body) + .json() + + return res + } catch (err) { + return {} + } } putWasmFileToS3 = (wasmFolder) => { @@ -271,51 +204,39 @@ module.exports = class S3Datastore extends Datastore { } } - putWasmInformationsToS3 = (email, pluginId, newHash, generateWasmName) => { - return this.getUser(email) - .then(data => { - const plugin = data.plugins.map(plugin => plugin.pluginId === pluginId) - - if (plugin.owner) - return this.putWasmInformationsToS3(plugin.owner, pluginId, newHash, generateWasmName) + pushNewPluginVersion = (email, pluginId, newHash, generateWasmName) => { + return this.getPlugin(email, pluginId) + .then(plugin => { + let versions = plugin.versions || []; - const newPlugins = data.plugins.map(plugin => { - if (plugin.pluginId !== pluginId) { - return plugin; - } - let versions = plugin.versions || []; + // convert legacy array + if (versions.length > 0 && isAString(versions[0])) { + versions = versions.map(name => ({ name })) + } - // convert legacy array - if (versions.length > 0 && isAString(versions[0])) { - versions = versions.map(name => ({ name })) + const index = versions.findIndex(item => item.name === generateWasmName); + if (index === -1) + versions.push({ + name: generateWasmName, + updated_at: Date.now(), + creator: email + }) + else { + versions[index] = { + ...versions[index], + updated_at: Date.now(), + creator: email } + } - const index = versions.findIndex(item => item.name === generateWasmName); - if (index === -1) - versions.push({ - name: generateWasmName, - updated_at: Date.now(), - creator: email - }) - else { - versions[index] = { - ...versions[index], - updated_at: Date.now(), - creator: email - } - } + const patchPlugin = { + ...plugin, + last_hash: newHash, + wasm: generateWasmName, + versions + } - return { - ...plugin, - last_hash: newHash, - wasm: generateWasmName, - versions - } - }); - return this.updateUser(email, { - ...data, - plugins: newPlugins - }) + return this.updatePluginInformations(pluginId, patchPlugin) }) } @@ -492,13 +413,8 @@ module.exports = class S3Datastore extends Datastore { } getConfigurations = (email, pluginId) => { - return this.getUser(email) - .then(data => { - const plugin = data.plugins.find(f => f.pluginId === pluginId) - - if (plugin.owner) - return this.getConfigurations(plugin.owner, pluginId) - + return this.getPlugin(email, pluginId) + .then(plugin => { const files = [{ ext: 'json', filename: 'config', @@ -528,57 +444,62 @@ module.exports = class S3Datastore extends Datastore { .catch(err => { logger.error(err) }); } - #deleteRootPlugin = async (plugin, pluginId) => { - const { admins, users } = plugin; - - await Promise.all([ - ...(admins || []), - ...(users || []) - ].map(user => this.#removePluginFromUser(user, pluginId))) - - return this.deletePlugin(plugin.owner, pluginId) - } - deletePlugin = (email, pluginId) => { - return new Promise(resolve => this.getUser(email) - .then(data => { - if (Object.keys(data).length > 0) { - const plugin = data.plugins.find(f => f.pluginId === pluginId) - - this.updateUser(email, { - ...data, - plugins: data.plugins.filter(f => f.pluginId !== pluginId) - }) - .then(() => { - if (plugin.owner || plugin.users || plugin.admins) { - this.#deleteRootPlugin(plugin, pluginId) - .then(resolve) - } else { - const pluginHash = (plugin || {}).last_hash; - Promise.all([ - this.deleteObject(`${pluginHash}.zip`), - this.deleteObject(`${pluginHash}-logs.zip`), - this.deleteObject(`${pluginId}.zip`), - this.deleteObject(`${pluginId}-logs.zip`), - this.removeBinaries((plugin.versions || []).map(r => r.name)) - ]) - .then(() => resolve({ status: 204, body: null })) - .catch(err => { - resolve({ - status: 400, - body: { - error: err.message - } - }) - }) + return new Promise(resolve => this.getPlugin(email, pluginId) + .then(plugin => { + Promise.all([ + this.deleteObject(`${pluginId}.zip`), + this.deleteObject(`${pluginId}-logs.zip`), + this.removeBinaries((plugin.versions || []).map(r => r.name)), + this.deleteObject(`${pluginId}.json`), + this.removePluginFromList(pluginId) + ]) + .then(() => resolve({ status: 204, body: null })) + .catch(err => { + resolve({ + status: 400, + body: { + error: err.message } }) - } else { - resolve({ - status: 204, body: null }) - } })) + .catch(_ => { + resolve({ + status: 400, + body: { + error: "something bad happened" + } + }) + }) + } + + updatePluginInformations = (id, body) => { + const { instance, Bucket } = this.#state; + + const params = { + Bucket, + Key: `${id}.json`, + ContentType: 'application/json', + Body: fromUtf8(JSON.stringify(body)) + } + + console.log("updatePluginInformations", id, body) + + return instance.send(new PutObjectCommand(params)) + .then(() => ({ + status: 204, + body: null + })) + .catch(err => { + return { + status: err.$metadata.httpStatusCode, + body: { + error: err.Code, + status: err.$metadata.httpStatusCode + } + } + }) } updatePlugin = (id, body) => { @@ -610,229 +531,198 @@ module.exports = class S3Datastore extends Datastore { const { instance, Bucket } = this.#state; return new Promise(resolve => { - this.createUserIfNotExists(email) - .then(() => { - this.getUser(email) - .then(data => { - const pluginId = crypto.randomUUID() - const plugins = [ - ...(data.plugins || []), - isGithub ? { - filename: metadata.repo, - owner: metadata.owner, - ref: metadata.ref, - type: 'github', - pluginId: pluginId, - private: metadata.private - } : { - filename: metadata.name.replace(/ /g, '-'), - type: metadata.type, - pluginId: pluginId, - template: metadata.template - } - - ] - const params = { - Bucket, - Key: `${format(email)}.json`, - Body: fromUtf8(JSON.stringify({ - ...data, - plugins - })), - ContentType: 'application/json', - } + const pluginId = crypto.randomUUID() + + const newPlugin = isGithub ? { + filename: metadata.repo, + owner: metadata.owner, + ref: metadata.ref, + type: 'github', + pluginId: pluginId, + private: metadata.private + } : { + filename: metadata.name.replace(/ /g, '-'), + type: metadata.type, + pluginId: pluginId, + template: metadata.template + }; + + console.log('generate pluginID', pluginId, newPlugin.pluginId) + + const params = { + Bucket, + Key: `${pluginId}.json`, + Body: fromUtf8(JSON.stringify(newPlugin)), + ContentType: 'application/json', + } - instance.send(new PutObjectCommand(params)) - .then(() => { - resolve({ - status: 201, - body: { - plugins - } - }) - }) - .catch(err => { - resolve({ - status: err.$metadata.httpStatusCode, - body: { - error: err.Code, - status: err.$metadata.httpStatusCode - } - }) - }) - }) + instance.send(new PutObjectCommand(params)) + .then(async () => { + await this.addPluginToList(email, newPlugin); + const plugins = await this.getUserPlugins(email); + resolve({ + status: 201, + body: { + plugins + } + }) }) .catch(err => { + console.log(err) resolve({ - status: 400, + status: err.$metadata.httpStatusCode, body: { - error: err.message + error: err.Code, + status: err.$metadata.httpStatusCode } }) }) }) } - patchPlugin = (email, pluginId, field, value) => { - return this.getUser(email) - .then(data => { - const plugin = (data.plugins || []).find(plugin => plugin.pluginId === pluginId) - - if (plugin.owner) - return this.patchPlugin(plugin.owner, pluginId, field, value) - else - return this.updateUser(email, { - ...data, - plugins: (data.plugins || []).map(plugin => { - if (plugin.pluginId === pluginId) { - return { - ...plugin, - [field]: value - } - } else { - return plugin - } - }) - }) - }) + patchPlugin = async (email, pluginId, field, value) => { + const plugin = await this.getPlugin(email, pluginId) + + return this.updatePluginInformations(pluginId, { + ...plugin, + [field]: value + }) } patchPluginName = (email, pluginId, newName) => { return this.patchPlugin(email, pluginId, 'filename', newName) } - #removePluginFromUser = async (email, pluginId) => { - const user = await this.getUser(email) + patchPluginUsers = async (email, pluginId, newUsers, newAdmins) => { + const plugins = await this.getPlugins() - return this.updateUser(email, { - ...user, - plugins: (user.plugins || []).filter(plugin => plugin.pluginId !== pluginId) - }) - } + const plugin = plugins.find(p => p.pluginId === pluginId) - patchPluginUsers = async (email, pluginId, newUsers, newAdmins) => { - let currentUser = email; + if (plugin) { + this.updatePluginList(pluginId, { + ...plugin, + users: newUsers, + admins: newAdmins + }) + return { + status: 204, + data: null + } + } else { + return { + status: 404, + data: { + error: 'something bad happened' + } + } + } + } - const user = await this.getUser(currentUser); + getUserPlugins = async email => { + const plugins = await this.getPlugins(); + console.log(plugins) - let plugin = user.plugins.find(plugin => plugin.pluginId === pluginId) + const userPlugins = plugins.filter(plugin => { + const users = plugin.users || []; + const admins = plugin.admins || []; - // if plugin is not owned by the user, we need to edit the root plugin - if (plugin.owner) { - currentUser = plugin.owner; + return users.includes(email) || admins.includes(email) + }) || [] - const owner = await this.getUser(plugin.owner) - plugin = owner.plugins.find(plugin => plugin.pluginId === pluginId) - } + console.log("users plugins") + console.log(userPlugins, email) - const removedUsers = (plugin.users || []).filter(user => !newUsers.includes(user) && !newAdmins.includes(user)) - const removedAdmins = (plugin.admins || []).filter(admin => !newAdmins.includes(admin) && !newUsers.includes(admin)) - - await Promise.all(removedUsers.map(user => this.#removePluginFromUser(user, pluginId))) - await Promise.all(removedAdmins.map(admin => this.#removePluginFromUser(admin, pluginId))) - - return this.getUser(currentUser) - .then(data => this.updateUser(currentUser, { - ...data, - plugins: (data.plugins || []).map(plugin => { - if (plugin.pluginId === pluginId) { - return { - ...plugin, - users: newUsers, - admins: newAdmins - } - } else { - return plugin - } - }) - })) + return Promise.all(userPlugins.map(plugin => this.getPlugin(email, plugin.pluginId))) + .then(plugins => { + return plugins.filter(data => Object.keys(data).length > 0) + }) } - acceptInvitation = async (userEmail, ownerId, pluginId) => { - try { - const ownerPlugins = await this.getUserPlugins(ownerId) - const user = await this.getUser(userEmail) - - const ownerPlugin = ownerPlugins.find(plugin => plugin.pluginId === pluginId) + addPluginToList = async (email, plugin) => { + const { instance, Bucket } = this.#state; - if (ownerPlugin && - ((ownerPlugin.users || []).includes(userEmail) || - (ownerPlugin.admins || []).includes(userEmail))) { + const plugins = await this.getPlugins() - const newPlugin = { - pluginId: ownerPlugin.pluginId, - owner: ownerId + return instance.send(new PutObjectCommand({ + Bucket, + Key: 'plugins.json', + ContentType: 'application/json', + Body: fromUtf8(JSON.stringify([ + ...plugins, + { + pluginId: plugin.pluginId, + admins: [email], + users: [] } - - return this.updateUser(userEmail, { - ...user, - plugins: [ - ...(user.plugins || []).filter(plugin => plugin.pluginId !== pluginId), - newPlugin - ] - }) - .then(() => true) - } else { - return false - } - } catch (err) { - console.log(err) - return false - } + ])) + })) } - getInvitation = async (userEmail, ownerId, pluginId) => { - try { - const ownerPlugins = await this.getUserPlugins(ownerId) + updatePluginList = async (pluginId, content) => { + const { instance, Bucket } = this.#state; - const ownerPlugin = ownerPlugins.find(plugin => plugin.pluginId === pluginId) + const plugins = await this.getPlugins() - return ownerPlugin - } catch (err) { - console.log(err) - return false - } + return instance.send(new PutObjectCommand({ + Bucket, + Key: 'plugins.json', + ContentType: 'application/json', + Body: fromUtf8(JSON.stringify(plugins.map(plugin => { + if (plugin.pluginId === pluginId) { + return content + } + return plugin + }))) + })) } - canSharePlugin = async (email, pluginId) => { - const user = await this.getUser(email); + removePluginFromList = async pluginId => { + const { instance, Bucket } = this.#state; + + const plugins = await this.getPlugins() - const plugin = user.plugins.find(plugin => plugin.pluginId === pluginId); + return instance.send(new PutObjectCommand({ + Bucket, + Key: 'plugins.json', + ContentType: 'application/json', + Body: fromUtf8(JSON.stringify(plugins.filter(plugin => plugin.pluginId !== pluginId))) + })) + } - if (plugin?.owner) { - const owner = await this.getUser(plugin.owner) + getInvitation = (email, pluginId) => this.getPlugin(email, pluginId) - const rootPlugin = owner.plugins.find(plugin => plugin.pluginId === pluginId); + canSharePlugin = async (email, pluginId) => { + const plugins = await this.getPlugins() - if (rootPlugin) { - return (rootPlugin.admins || []).includes(email) - } + const plugin = plugins.find(p => p.pluginId === pluginId) - return false + if (plugin) { + return plugin.admins.includes(email) } - return true + return false } getPluginUsers = async (email, pluginId) => { - const user = await this.getUser(email); + const plugins = await this.getPlugins() - const plugin = user.plugins.find(plugin => plugin.pluginId === pluginId); - - if (plugin.owner) { - const owner = await this.getUser(plugin.owner) - - const rootPlugin = owner.plugins.find(plugin => plugin.pluginId === pluginId); + const plugin = plugins.find(p => p.pluginId === pluginId) + if (plugin) { + return { + status: 200, + data: { + admins: plugin.admins, + users: plugin.users + } + } + } else { return { - admins: rootPlugin.admins, - users: rootPlugin.users + status: 404, + data: { + error: 'something bad happened' + } } } - - return { - admins: plugin.admins, - users: plugin.users - } } }; diff --git a/server/routers/invitation.js b/server/routers/invitation.js index 1780a78..b0afc07 100755 --- a/server/routers/invitation.js +++ b/server/routers/invitation.js @@ -4,43 +4,38 @@ const Datastore = require('../datastores'); const router = express.Router() router.get('/:id', (req, res) => { - const [userId, pluginId] = Buffer + const pluginId = Buffer .from(req.params.id, 'base64') .toString('ascii') - .split(":") - if (req.user.email !== userId) { - Datastore.getInvitation(req.user.email, userId, pluginId) - .then(body => { - if (body) { - return res.json(body) - } else { - return res.status(400).json({ error: 'something wrong happened' }) - } - }) - } else { - res.status(400).json({ error: 'something wrong happened' }) - } + Datastore.getInvitation(req.user.email, pluginId) + .then(body => { + if (body) { + return res.json(body) + } else { + return res.status(400).json({ error: 'something wrong happened' }) + } + }) + .catch(err => { + if (err === "Not authorized") { + res.redirect('/') + } + }) }) -router.post('/:hash', (req, res) => { - const [userId, pluginId] = Buffer - .from(req.params.hash, 'base64') - .toString('ascii') - .split(":") +// router.post('/:hash', (req, res) => { +// const pluginId = Buffer +// .from(req.params.id, 'base64') +// .toString('ascii') - if (req.user.email !== userId) { - Datastore.acceptInvitation(req.user.email, userId, pluginId) - .then(accepted => { - if (accepted) { - res.redirect(`/?pluginId=${pluginId}`) - } else { - res.redirect('/') - } - }) - } else { - res.redirect('/') - } -}) +// Datastore.acceptInvitation(req.user.email, pluginId) +// .then(accepted => { +// if (accepted) { +// res.redirect(`/?pluginId=${pluginId}`) +// } else { +// res.redirect('/') +// } +// }) +// }) module.exports = router diff --git a/server/routers/plugins.js b/server/routers/plugins.js index b46c67d..9e91b6a 100755 --- a/server/routers/plugins.js +++ b/server/routers/plugins.js @@ -148,11 +148,7 @@ router.put('/:id', (req, res) => { }) router.get('/:id/share-links', async (req, res) => { - const plugins = await Datastore.getUserPlugins(req.user.email) - - const owner = plugins.find(plugin => plugin.pluginId === req.params.id)?.owner || req.user.email - - const hash = Buffer.from(`${owner}:${req.params.id}`).toString('base64') + const hash = Buffer.from(req.params.id).toString('base64') res.json(`${ENV.SECURE_DOMAIN ? 'https' : 'http'}://${ENV.DOMAIN}:${ENV.EXPOSED_PORT || ENV.PORT}/invitation/${hash}`) }) @@ -160,17 +156,17 @@ router.put('/:id/users', (req, res) => { Datastore.patchPluginUsers(req.user.email, req.params.id, req.body.users, - [...new Set([...req.body.admins, req.user.email])]) - .then(() => { + req.body.admins) + .then(data => { res - .status(204) - .json(null) + .status(data.status) + .json(data.body) }) }) router.get('/:id/users', (req, res) => { Datastore.getPluginUsers(req.user.email, req.params.id) - .then(members => res.status(200).json(members)) + .then(out => res.status(out.status).json(out.data)) }) router.get('/:id/rights', (req, res) => { @@ -343,15 +339,9 @@ router.post('/:id/build', async (req, res) => { const pluginId = req.params.id; const release = req.query.release === 'true'; - let user = req.user ? req.user.email : 'admin@otoroshi.io' - - const data = await Datastore.getUser(user) - let plugin = (data.plugins || []).find(p => p.pluginId === pluginId); + let user = req.user ? req.user.email : 'admin@otoroshi.io'; - if (plugin?.owner) { - user = plugin.owner; - plugin = await Datastore.getPlugin(plugin.owner, pluginId); - } + const plugin = await Datastore.getPlugin(user, pluginId); if (plugin.type === 'github') { plugin.type = req.query.plugin_type; diff --git a/server/routers/public.js b/server/routers/public.js index e7b9e6c..20079e9 100755 --- a/server/routers/public.js +++ b/server/routers/public.js @@ -67,27 +67,10 @@ router.get('/wasm/:id', (req, res) => { router.get('/plugins', (req, res) => { const reg = req.headers['kind'] || '*'; - if (reg === '*') { - Datastore.getUsers() - .then(r => { - const users = [...new Set([...(r || []), "adminotoroshiio"])]; - - if (users.length > 0) { - Promise.all(users.map(Datastore.getUser)) - .then(pluginsByUser => { - res.json(pluginsByUser - .map(user => user.plugins) - .flat() - .filter(f => f)) - }) - } else { - res.json([]) - } - }) - } else { - Datastore.getUser(reg) - .then(data => res.json(data.plugins)) - } + Datastore + .getPlugins() + .then(plugins => Promise.all(plugins.map(plugin => Datastore.getPlugin(reg, plugin.pluginId)))) + .then(plugins => res.json(plugins.filter(data => Object.keys(data).length > 0))) }); module.exports = router; \ No newline at end of file diff --git a/server/services/compiler/compiler.js b/server/services/compiler/compiler.js index d98e23e..7693c64 100755 --- a/server/services/compiler/compiler.js +++ b/server/services/compiler/compiler.js @@ -155,7 +155,7 @@ class Compiler { .then(() => this.#websocketEmitMessage(buildOptions, "WASM has been saved ...")), Datastore.putBuildLogsToS3(`${buildOptions.plugin.id}-logs.zip`, buildOptions.logsFolder) .then(() => this.#websocketEmitMessage(buildOptions, "Logs has been saved ...")), - Datastore.putWasmInformationsToS3(buildOptions.userEmail, buildOptions.plugin.id, buildOptions.plugin.hash, `${this.options.wasmName}.wasm`) + Datastore.pushNewPluginVersion(buildOptions.userEmail, buildOptions.plugin.id, buildOptions.plugin.hash, `${this.options.wasmName}.wasm`) .then(() => this.#websocketEmitMessage(buildOptions, "Informations has been updated")) ])) .then(() => { diff --git a/ui/src/App.js b/ui/src/App.js index 914d269..7a6b67f 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -14,7 +14,7 @@ class App extends React.Component { plugins: [], selectedPlugin: undefined, configFiles: [], - version: 'unknown' + version: '' } componentDidMount() { diff --git a/ui/src/InvitationHandler.js b/ui/src/InvitationHandler.js index eed498c..d020973 100644 --- a/ui/src/InvitationHandler.js +++ b/ui/src/InvitationHandler.js @@ -18,13 +18,21 @@ function InvitationHandler() { const paths = window.location.pathname.split("/invitation"); if (paths.length === 2) { - const invitationId = paths[1]; + const invitationId = paths[1].slice(1); Service.getInvitationInformation(invitationId) - .then(invit => setInvitation({ - ...invit, - invitationId - })) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } else { + response + .json() + .then(invit => setInvitation({ + ...invit, + invitationId + })) + } + }) } }, []) @@ -38,7 +46,7 @@ function InvitationHandler() { }}> diff --git a/ui/src/TabsHeader.js b/ui/src/TabsHeader.js index 98cce4a..a7abcf9 100644 --- a/ui/src/TabsHeader.js +++ b/ui/src/TabsHeader.js @@ -6,7 +6,7 @@ import { toast } from 'react-toastify' import Select from 'react-select/creatable'; export function TabsHeader({ - selectedPlugin, onSave, onBuild, onDownload, + selectedPlugin, onSave, onBuild, showPlaySettings, children }) { if (!selectedPlugin?.pluginId) @@ -16,20 +16,15 @@ export function TabsHeader({ selectedPluginType={selectedPlugin?.type} onSave={onSave} onBuild={onBuild} - onDownload={onDownload} showActions={!!selectedPlugin} showPlaySettings={showPlaySettings} pluginId={selectedPlugin?.pluginId} - // users={selectedPlugin?.users} - // admins={selectedPlugin?.admins} > {children} } -function Header({ - children, onSave, onBuild, showActions, onDownload, - showPlaySettings, pluginId }) { +function Header({ children, onSave, onBuild, showActions, showPlaySettings, pluginId }) { const [runtimeState, setRuntimeEnvironment] = useState(false); const [canShare, setCanSharePlugin] = useState(false); @@ -73,7 +68,6 @@ function Header({ - {runtimeState && } }
    @@ -213,9 +207,9 @@ function ShareButton(props) { @@ -248,16 +242,7 @@ function Release({ onBuild }) { tooltip="Release" className="navbar-item" onClick={() => onBuild(true)}> - - -} - -function Download({ onDownload }) { - return } diff --git a/ui/src/TabsManager.js b/ui/src/TabsManager.js index 615287d..a04285c 100644 --- a/ui/src/TabsManager.js +++ b/ui/src/TabsManager.js @@ -70,6 +70,7 @@ function TabsManager({ plugins, ...props }) { onNewPlugin={props.onNewPlugin} setFilename={props.onPluginNameChange} removePlugin={props.removePlugin} + onDownload={props.onDownload} enablePluginRenaming={props.enablePluginRenaming} /> {props.selectedPlugin && } - {props.selectedPlugin && } + {props.selectedPlugin &&
    + + +
    } } diff --git a/ui/src/index.css b/ui/src/index.css index a706e20..946b09d 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -73,6 +73,7 @@ h1 { [tooltip]::before, [tooltip]::after { text-transform: none; + right: -50%; font-size: 0.75em; line-height: 1; user-select: none; diff --git a/ui/src/services/index.js b/ui/src/services/index.js index c1d8324..2701dfa 100644 --- a/ui/src/services/index.js +++ b/ui/src/services/index.js @@ -134,7 +134,7 @@ export const getWasmRelease = wasmId => rawFetch(`/wasm/${wasmId}`); export const getAppVersion = () => f('/version'); -export const getInvitationInformation = invitationId => jsonFetch(`/invitations/${invitationId}`) +export const getInvitationInformation = invitationId => f(`/invitations/${invitationId}`) export const acceptInvitation = invitationId => f(`/invitations/${invitationId}`, { method: 'POST',