diff --git a/src/backend/exports.js b/src/backend/exports.js index 77bc94024..a8649af66 100644 --- a/src/backend/exports.js +++ b/src/backend/exports.js @@ -25,6 +25,7 @@ const PuterDriversModule = require("./src/PuterDriversModule.js"); const { testlaunch } = require("./src/index.js"); const BaseService = require("./src/services/BaseService.js"); const { Context } = require("./src/util/context.js"); +const { TestDriversModule } = require("./src/modules/test-drivers/TestDriversModule.js"); module.exports = { @@ -46,4 +47,5 @@ module.exports = { PuterDriversModule, LocalDiskStorageModule, SelfHostedModule, + TestDriversModule, }; diff --git a/src/backend/src/data/hardcoded-permissions.js b/src/backend/src/data/hardcoded-permissions.js index 26ba332fb..8308d123f 100644 --- a/src/backend/src/data/hardcoded-permissions.js +++ b/src/backend/src/data/hardcoded-permissions.js @@ -72,6 +72,9 @@ const policy_perm = selector => ({ const hardcoded_user_group_permissions = { system: { + 'ca342a5e-b13d-4dee-9048-58b11a57cc55': { + 'service': {}, + }, 'b7220104-7905-4985-b996-649fdcdb3c8f': { 'service:hello-world:ii:hello-world': policy_perm('temp.es'), 'driver:puter-kvstore': policy_perm('temp.kv'), diff --git a/src/backend/src/modules/test-drivers/TestAssetHostService.js b/src/backend/src/modules/test-drivers/TestAssetHostService.js new file mode 100644 index 000000000..dd991959e --- /dev/null +++ b/src/backend/src/modules/test-drivers/TestAssetHostService.js @@ -0,0 +1,16 @@ +const BaseService = require("../../services/BaseService"); + +class TestAssetHostService extends BaseService { + async ['__on_install.routes'] () { + const { app } = this.services.get('web-server'); + const path_ = require('node:path'); + + app.use('/test-assets', require('express').static( + path_.join(__dirname, 'assets') + )); + } +} + +module.exports = { + TestAssetHostService +}; diff --git a/src/backend/src/modules/test-drivers/TestDriversModule.js b/src/backend/src/modules/test-drivers/TestDriversModule.js new file mode 100644 index 000000000..609324a18 --- /dev/null +++ b/src/backend/src/modules/test-drivers/TestDriversModule.js @@ -0,0 +1,17 @@ +const { AdvancedBase } = require("@heyputer/puter-js-common"); + +class TestDriversModule extends AdvancedBase { + async install (context) { + const services = context.get('services'); + + const { TestAssetHostService } = require('./TestAssetHostService') + services.registerService('__test-assets', TestAssetHostService); + + const { TestImageService } = require('./TestImageService'); + services.registerService('test-image', TestImageService); + } +} + +module.exports = { + TestDriversModule, +}; diff --git a/src/backend/src/modules/test-drivers/TestImageService.js b/src/backend/src/modules/test-drivers/TestImageService.js new file mode 100644 index 000000000..4c0603042 --- /dev/null +++ b/src/backend/src/modules/test-drivers/TestImageService.js @@ -0,0 +1,85 @@ +const config = require("../../config"); +const BaseService = require("../../services/BaseService"); +const { TypedValue } = require("../../services/drivers/meta/Runtime"); +const { buffer_to_stream } = require("../../util/streamutil"); + +const PUBLIC_DOMAIN_IMAGES = [ + { + name: 'starry-night', + url: 'https://upload.wikimedia.org/wikipedia/commons/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg', + file: 'starry.jpg', + }, +]; + +class TestImageService extends BaseService { + async ['__on_driver.register.interfaces'] () { + const svc_registry = this.services.get('registry'); + const col_interfaces = svc_registry.get('interfaces'); + + col_interfaces.set('test-image', { + methods: { + echo_image: { + parameters: { + source: { + type: 'file', + }, + }, + result: { + type: { + $: 'stream', + content_type: 'image' + }, + }, + }, + get_image: { + parameters: { + source_type: { + type: 'string' + }, + }, + result: { + type: { + $: 'stream', + content_type: 'image' + } + } + } + } + }); + } + + static IMPLEMENTS = { + ['version']: { + get_version () { + return 'v1.0.0'; + } + }, + ['test-image']: { + async echo_image ({ + source, + }) { + const stream = await source.get('stream'); + return new TypedValue({ + $: 'stream', + content_type: 'image/jpeg' + }, stream); + }, + async get_image ({ + source_type, + }) { + const image = PUBLIC_DOMAIN_IMAGES[0]; + if ( source_type === 'string:url:web' ) { + return new TypedValue({ + $: 'string:url:web', + content_type: 'image', + }, `${config.origin}/test-assets/${image.file}`); + } + throw new Error('not implemented yet'); + } + }, + } +} + +module.exports = { + TestImageService +}; diff --git a/src/backend/src/modules/test-drivers/assets/starry.jpg b/src/backend/src/modules/test-drivers/assets/starry.jpg new file mode 100644 index 000000000..9a24b899d Binary files /dev/null and b/src/backend/src/modules/test-drivers/assets/starry.jpg differ diff --git a/src/backend/src/modules/test-drivers/assets/wave.jpg b/src/backend/src/modules/test-drivers/assets/wave.jpg new file mode 100644 index 000000000..cf6a3940d Binary files /dev/null and b/src/backend/src/modules/test-drivers/assets/wave.jpg differ diff --git a/src/backend/src/modules/test-drivers/doc/requests.md b/src/backend/src/modules/test-drivers/doc/requests.md new file mode 100644 index 000000000..39275fb9b --- /dev/null +++ b/src/backend/src/modules/test-drivers/doc/requests.md @@ -0,0 +1,98 @@ +```javascript +blob = await (await fetch("http://api.puter.localhost:4100/drivers/call", { + "headers": { + "Content-Type": "application/json", + "Authorization": `Bearer ${puter.authToken}`, + }, + "body": JSON.stringify({ + interface: 'test-image', + method: 'get_image', + args: { + source_type: 'string:url:web' + } + }), + "method": "POST", +})).blob(); +dataurl = await new Promise((y, n) => { + a = new FileReader(); + a.onload = _ => y(a.result); + a.onerror = _ => n(a.error); + a.readAsDataURL(blob) +}); +URL.createObjectURL(await (await fetch("http://api.puter.localhost:4100/drivers/call", { + "headers": { + "Content-Type": "application/json", + "Authorization": `Bearer ${puter.authToken}`, + }, + "body": JSON.stringify({ + interface: 'test-image', + method: 'echo_image', + args: { + source: dataurl, + } + }), + "method": "POST", +})).blob()); +``` + +```javascript +await(async () => { + + blob = await (await fetch("http://api.puter.localhost:4100/drivers/call", { + "headers": { + "Content-Type": "application/json", + "Authorization": `Bearer ${puter.authToken}`, + }, + "body": JSON.stringify({ + interface: 'test-image', + method: 'get_image', + args: { + source_type: 'string:url:web' + } + }), + "method": "POST", + })).blob(); + + const endpoint = 'http://api.puter.localhost:4100/drivers/call'; + + const body = { + object: { + interface: 'test-image', + method: 'echo_image', + ['args.source']: { + $: 'file', + size: blob.size, + type: blob.type, + }, + }, + file: [ + blob, + ] + }; + + const formData = new FormData(); + for ( const k in body ) { + console.log('k', k); + const append = v => { + if ( v instanceof Blob ) { + formData.append(k, v, 'filename'); + } else { + formData.append(k, JSON.stringify(v)); + } + }; + if ( Array.isArray(body[k]) ) { + for ( const v of body[k] ) append(v); + } else { + append(body[k]); + } + } + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Authorization': `Bearer ${puter.authToken}` }, + body: formData + }); + const echo_blob = await response.blob(); + const echo_url = URL.createObjectURL(echo_blob); + return echo_url; +})(); +``` \ No newline at end of file diff --git a/src/backend/src/routers/drivers/call.js b/src/backend/src/routers/drivers/call.js index 8d0ff7aa9..737231af1 100644 --- a/src/backend/src/routers/drivers/call.js +++ b/src/backend/src/routers/drivers/call.js @@ -16,12 +16,15 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +const APIError = require("../../api/APIError"); const eggspress = require("../../api/eggspress"); const { FileFacade } = require("../../services/drivers/FileFacade"); const { TypeSpec } = require("../../services/drivers/meta/Construct"); const { TypedValue } = require("../../services/drivers/meta/Runtime"); const { Context } = require("../../util/context"); +const { whatis } = require("../../util/langutil"); const { TeePromise } = require("../../util/promise"); +const { valid_file_size } = require("../../util/validutil"); let _handle_multipart; @@ -55,12 +58,14 @@ module.exports = eggspress('/drivers/call', { const x = Context.get(); const svc_driver = x.get('services').get('driver'); - const interface_name = req.body.interface; - const test_mode = req.body.test_mode; + let p_request = null; + let body; + if ( req.headers['content-type'].includes('multipart/form-data') ) { + ({ params: body, p_data_end: p_request } = await _handle_multipart(req)); + } else body = req.body; - const args = req.headers['content-type'].includes('multipart/form-data') - ? await _handle_multipart(req) - : req.body.args; + const interface_name = body.interface; + const test_mode = body.test_mode; let context = Context.get(); if ( test_mode ) context = context.sub({ test_mode: true }); @@ -68,12 +73,22 @@ module.exports = eggspress('/drivers/call', { const result = await context.arun(async () => { return await svc_driver.call({ iface: interface_name, - method: req.body.method, - args + method: body.method, + format: body.format, + args: body.args, }); }); + // We can't wait for the request to finish before responding; + // consider the case where a driver method implements a + // stream transformation, thus the stream from the request isn't + // consumed until the response is being sent. + _respond(res, result); + + // What we _can_ do is await the request promise while responding + // to ensure errors are caught here. + await p_request; }); const _respond = (res, result) => { @@ -96,49 +111,86 @@ const _respond = (res, result) => { }; _handle_multipart = async (req) => { - const busboy = require('busboy'); - const { Readable } = require('stream'); + const Busboy = require('busboy'); + const { PassThrough } = require('stream'); const params = {}; + const files = []; + let file_index = 0; - const bb = new busboy({ + const bb = Busboy({ headers: req.headers, }); - const p_ready = new TeePromise(); + const p_data_end = new TeePromise(); + const p_nonfile_data_end = new TeePromise(); bb.on('file', (fieldname, stream, details) => { - const file_facade = new FileFacade(); - file_facade.values.set('stream', stream); - file_facade.values.set('busboy:details', details); - if ( params.hasOwnProperty(fieldname) ) { - if ( ! Array.isArray(params[fieldname]) ) { - params[fieldname] = [params[fieldname]]; - } - params[fieldname].push(file_facade); - } else { - params[fieldname] = file_facade; - } + p_nonfile_data_end.resolve(); + const fileinfo = files[file_index++]; + stream.pipe(fileinfo.stream); }); - bb.on('field', (fieldname, value, details) => { - if ( params.hasOwnProperty(fieldname) ) { - if ( ! Array.isArray(params[fieldname]) ) { - params[fieldname] = [params[fieldname]]; + + const on_field = (fieldname, value) => { + const key_parts = fieldname.split('.'); + const last_key = key_parts.pop(); + let dst = params; + for ( let i = 0; i < key_parts.length; i++ ) { + if ( ! dst.hasOwnProperty(key_parts[i]) ) { + dst[key_parts[i]] = {}; } - params[fieldname].push(value); + if ( whatis(dst[key_parts[i]]) !== 'object' ) { + throw new Error( + `Tried to set member of non-object: ${key_parts[i]} in ${fieldname}` + ); + } + dst = dst[key_parts[i]]; + } + if ( whatis(value) === 'object' && value.$ === 'file' ) { + const fileinfo = value; + const { v: size, ok: size_ok } = + valid_file_size(fileinfo.size); + if ( ! size_ok ) { + throw APIError.create('invalid_file_metadata'); + } + fileinfo.size = size; + fileinfo.stream = new PassThrough(); + const file_facade = new FileFacade(); + file_facade.values.set('stream', fileinfo.stream); + fileinfo.facade = file_facade, + files.push(fileinfo); + value = file_facade; + } + if ( dst.hasOwnProperty(last_key) ) { + if ( ! Array.isArray(dst[last_key]) ) { + dst[last_key] = [dst[last_key]]; + } + dst[last_key].push(value); } else { - params[fieldname] = value; + dst[last_key] = value; + } + }; + + bb.on('field', (fieldname, value, details) => { + const o = JSON.parse(value); + for ( const k in o ) { + on_field(k, o[k]); } }); bb.on('error', (err) => { - p_ready.reject(err); + p_data_end.reject(err); }); bb.on('close', () => { - p_ready.resolve(); + p_data_end.resolve(); }); req.pipe(bb); - await p_ready; + (async () => { + await p_data_end; + p_nonfile_data_end.resolve(); + })(); - return params; -} \ No newline at end of file + await p_nonfile_data_end; + + return { params, p_data_end }; +} diff --git a/src/backend/src/services/drivers/CoercionService.js b/src/backend/src/services/drivers/CoercionService.js index bd64018b5..c3784e178 100644 --- a/src/backend/src/services/drivers/CoercionService.js +++ b/src/backend/src/services/drivers/CoercionService.js @@ -40,6 +40,7 @@ class CoercionService extends BaseService { content_type: 'image' }, coerce: async typed_value => { + this.log.noticeme('coercion is running!'); const response = await CoercionService.MODULES.axios.get(typed_value.value, { responseType: 'stream', }); diff --git a/src/backend/src/services/drivers/DriverService.js b/src/backend/src/services/drivers/DriverService.js index 510b017fc..c9f427759 100644 --- a/src/backend/src/services/drivers/DriverService.js +++ b/src/backend/src/services/drivers/DriverService.js @@ -111,6 +111,7 @@ class DriverService extends BaseService { } async _call ({ driver, iface, method, args }) { + console.log('??', driver, iface, method, args); const processed_args = await this._process_args(iface, method, args); if ( Context.get('test_mode') ) { processed_args.test_mode = true; @@ -293,7 +294,7 @@ class DriverService extends BaseService { { name: 'enforce logical rate-limit', on_call: async args => { - if ( ! effective_policy['rate-limit'] ) return args; + if ( ! effective_policy?.['rate-limit'] ) return args; const svc_su = this.services.get('su'); const svc_rateLimit = this.services.get('rate-limit'); await svc_su.sudo(policy_holder, async () => { @@ -309,7 +310,7 @@ class DriverService extends BaseService { { name: 'enforce monthly usage limit', on_call: async args => { - if ( ! effective_policy['monthly-limit'] ) return args; + if ( ! effective_policy?.['monthly-limit'] ) return args; const svc_monthlyUsage = services.get('monthly-usage'); const count = await svc_monthlyUsage.check_2( actor, method_key @@ -356,9 +357,18 @@ class DriverService extends BaseService { name: 'result coercion', on_return: async (result) => { if ( result instanceof TypedValue ) { + const svc_registry = this.services.get('registry'); + const c_interfaces = svc_registry.get('interfaces'); + + console.log('????--1', iface); const interface_ = c_interfaces.get(iface); - let desired_type = interface_.methods[method] - .result_choices[0].type; + console.log('????--2', interface_); + const method_spec = interface_.methods[method]; + let desired_type = + method_spec.result_choices + ? method_spec.result_choices[0].type + : method_spec.result.type + ; const svc_coercion = services.get('coercion'); result = await svc_coercion.coerce(desired_type, result); } diff --git a/src/backend/src/services/drivers/types.js b/src/backend/src/services/drivers/types.js index cf3813852..1c227bd9f 100644 --- a/src/backend/src/services/drivers/types.js +++ b/src/backend/src/services/drivers/types.js @@ -98,6 +98,10 @@ class File extends BaseType { } async consolidate (ctx, input, { arg_name }) { + if ( input instanceof FileFacade ) { + return input; + } + const result = new FileFacade(); // DRY: Part of this is duplicating FSNodeParam, but FSNodeParam is // subject to change in PR #647, so this should be updated later. diff --git a/src/gui/src/i18n/translations/ko.js b/src/gui/src/i18n/translations/ko.js index e2d78c5d9..47fca4779 100644 --- a/src/gui/src/i18n/translations/ko.js +++ b/src/gui/src/i18n/translations/ko.js @@ -349,4 +349,4 @@ const ko = { }, }; -export default ko; +export default ko; \ No newline at end of file diff --git a/src/gui/src/i18n/translations/nl.js b/src/gui/src/i18n/translations/nl.js index d02da55d2..e4bc171b0 100644 --- a/src/gui/src/i18n/translations/nl.js +++ b/src/gui/src/i18n/translations/nl.js @@ -18,190 +18,334 @@ */ const nl = { - name: "Nederlands", - english_name: "Dutch", - code: "nl", + name: 'Nederlands', + english_name: 'Dutch', + code: 'nl', dictionary: { - about: "Over", - account: "Account", - access_granted_to: "Toegang gegeven aan", - add_existing_account: "Bestaand Account Toevoegen", + about: 'Over', + account: 'Account', + account_password: 'Verifieer Account Wachtwoord', + access_granted_to: 'Toegang gegeven aan', + add_existing_account: 'Bestaand Account Toevoegen', all_fields_required: 'Alle velden zijn vereist.', - apply: "Toepassen", + allow: 'Toestaan', + apply: 'Toepassen', ascending: 'Oplopend', + associated_websites: 'Geassocieerde Websites', auto_arrange: 'Automatisch sorteren', - background: "Achtergrond", - browse: "Verkennen", + background: 'Achtergrond', + browse: 'Bladeren', cancel: 'Annuleren', center: 'Centreren', - change_desktop_background: 'Achtergrond veranderen…', - change_language: "Taal veranderen", - change_password: "Wachtwoord veranderen", - change_username: "Gebruikersnaam veranderen", - close_all_windows: "Alle schermen sluiten", - close_all_windows_and_log_out: 'Alle schermen sluiten en afmelden', - change_always_open_with: "Dit type bestand altijd openen met", + change_desktop_background: 'Bureaubladachtergrond veranderen…', + change_email: 'E-mail Wijzigen', + change_language: 'Taal veranderen', + change_password: 'Wachtwoord veranderen', + change_ui_colors: 'UI Kleuren veranderen', + change_username: 'Gebruikersnaam veranderen', + close: 'Sluiten', + close_all_windows: 'Alle vensters sluiten', + close_all_windows_confirm: 'Weet u zeker dat u alle vensters wilt sluiten?', + close_all_windows_and_log_out: 'Vensters sluiten en afmelden', + change_always_open_with: 'Wilt u dit bestandstype altijd openen met', color: 'Kleur', confirm: 'Bevestig', - confirm_account_for_free_referral_storage_c2a: 'Maak een account en bevestig uw emailadres om 1 GB aan gratis opslag te ontvangen. Uw vriend krijgt ook 1 GB aan gratis opslag.', - confirm_delete_multiple_items: 'Weet u zeker dat u deze bestanden permanent wilt verwijderen?', - confirm_delete_single_item: 'Weet u zeker dat u dit bestand permanent wilt verwijderen?', - confirm_delete_user: 'Weet u zeker dat u uw account wilt verwijderen? Al uw bestanden en gegevens zal permanent verwijderd worden. Deze actie kan niet ongedaan gemaakt worden.', - confirm_open_apps_log_out: 'U heeft nog geopende applicaties. Weet u zeker dat u wilt uitloggen?', - confirm_new_password: "Bevestig Nieuw Wachtwoord", - contact_us: "Neem contact met ons op", + confirm_2fa_setup: 'Ik heb de code toegevoegd aan mijn authenticatie-app', + confirm_2fa_recovery: 'Ik heb mijn herstelcodes op een veilige plaats opgeslagen', + confirm_account_for_free_referral_storage_c2a: 'Maak een account aan en bevestig uw e-mailadres om 1 GB gratis opslag te ontvangen. Uw vriend krijgt ook 1 GB gratis opslag.', + confirm_code_generic_incorrect: 'Onjuiste code.', + confirm_code_generic_too_many_requests: 'Te veel aanvragen. Wacht een paar minuten.', + confirm_code_generic_submit: 'Code invoeren', + confirm_code_generic_try_again: 'Probeer opnieuw', + confirm_code_generic_title: 'Bevestigingscode Invoeren', + confirm_code_2fa_instruction: 'Voer de 6-cijferige code in van uw authenticatie-app.', + confirm_code_2fa_submit_btn: 'Verzenden', + confirm_code_2fa_title: 'Voer 2FA Code In', + confirm_delete_multiple_items: 'Weet u zeker dat u deze items permanent wilt verwijderen?', + confirm_delete_single_item: 'Wilt u dit item permanent verwijderen?', + confirm_open_apps_log_out: 'U heeft geopende apps. Weet u zeker dat u wilt uitloggen?', + confirm_new_password: 'Bevestig Nieuw Wachtwoord', + confirm_delete_user: 'Weet u zeker dat u uw account wilt verwijderen? Al uw bestanden en gegevens worden permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.', + confirm_delete_user_title: 'Account Verwijderen?', + confirm_session_revoke: 'Weet u zeker dat u deze sessie wilt intrekken?', + confirm_your_email_address: 'Bevestig Uw E-mailadres', + contact_us: 'Neem Contact Op', + contact_us_verification_required: 'U moet een geverifieerd e-mailadres hebben om dit te gebruiken.', contain: 'Bevatten', - continue: "Doorgaan", + continue: 'Doorgaan', copy: 'Kopiëren', - copy_link: "Kopieer koppeling", - copying: "Kopiëren", + copy_link: 'Link Kopiëren', + copying: 'Kopiëren', + copying_file: 'Bestand Kopiëren %%', cover: 'Omslag', - create_account: "Maak Account", - create_free_account: "Maak Gratis Account", - create_shortcut: "Maak Snelkoppeling", - credits: "Credits", - current_password: "Huidig Wachtwoord", + create_account: 'Account Aanmaken', + create_free_account: 'Gratis Account Aanmaken', + create_shortcut: 'Snelkoppeling Maken', + credits: 'Credits', + current_password: 'Huidig Wachtwoord', cut: 'Knippen', - date_modified: 'Datum gewijzigd', + clock: 'Klok', + clock_visible_hide: 'Verbergen - Altijd verborgen', + clock_visible_show: 'Weergeven - Altijd zichtbaar', + clock_visible_auto: 'Auto - Standaard, alleen zichtbaar in de volledige schermmodus.', + close_all: 'Alles Sluiten', + created: 'Gemaakt', + date_modified: 'Datum Gewijzigd', + default: 'Standaard', delete: 'Verwijderen', - delete_account: "Account Verwijderen", - delete_permanently: "Permanent Verwijderen", - deploy_as_app: 'Uitrollen als applicatie', + delete_account: 'Account Verwijderen', + delete_permanently: 'Permanent Verwijderen', + deleting_file: 'Bestand Verwijderen %%', + deploy_as_app: 'Uitrollen als app', descending: 'Aflopend', - desktop_background_fit: "Passend", - developers: "Ontwikkelaars", - dir_published_as_website: `%strong% is gepubliceerd naar:`, - disassociate_dir: "Map Ontkoppelen", + desktop: 'Bureaublad', + desktop_background_fit: 'Aanpassen', + developers: 'Ontwikkelaars', + dir_published_as_website: `%strong% is gepubliceerd op:`, + disable_2fa: '2FA Uitschakelen', + disable_2fa_confirm: 'Weet u zeker dat u 2FA wilt uitschakelen?', + disable_2fa_instructions: 'Voer uw wachtwoord in om 2FA uit te schakelen.', + disassociate_dir: 'Map Loskoppelen', + documents: 'Documenten', + dont_allow: 'Niet Toestaan', download: 'Downloaden', download_file: 'Bestand Downloaden', - downloading: "Aan het downloaden", - email: "Emailadres", - email_or_username: "Emailadres of Gebruikersnaam", - empty_trash: 'Prullenbak Legen', - empty_trash_confirmation: `Weet u zeker dat u alle bestanden in de Prullenbak permanent wilt verwijderen?`, + downloading: 'Downloaden', + email: 'E-mail', + email_change_confirmation_sent: 'Er is een bevestigingsmail gestuurd naar uw nieuwe e-mailadres. Controleer uw inbox en volg de instructies om het proces te voltooien.', + email_invalid: 'E-mail is ongeldig.', + email_or_username: 'E-mail of Gebruikersnaam', + email_required: 'E-mail is verplicht.', + empty_trash: 'Prullenbak Leegmaken', + empty_trash_confirmation: `Weet u zeker dat u de items in de prullenbak permanent wilt verwijderen?`, emptying_trash: 'Prullenbak Legen…', - enter_password_to_confirm_delete_user: "Voer uw wachtwoord in om te bevestigen dat uw account verwijderd wordt", - feedback: "Feedback", - feedback_c2a: "Gebruik alstublieft het onderstaande formulier om ons uw feedback, opmerkingen en foutmeldingsrapporten te sturen.", - feedback_sent_confirmation: "Bedankt dat u contact met ons opneemt. Als er een emailadres aan uw account is gekoppeld, hoort u zo snel mogelijk van ons terug.", - forgot_pass_c2a: "Wachtwoord Vergeten?", - from: "Van", - general: "Algemeen", - get_a_copy_of_on_puter: `Bemachtig een kopie van '%%' op Puter.com!`, - get_copy_link: 'Krijg Kopieer Koppeling', - hide_all_windows: "Verberg alle Vensters", + enable_2fa: '2FA Inschakelen', + end_hard: 'Hard Stoppen', + end_process_force_confirm: 'Weet u zeker dat u dit proces geforceerd wilt beëindigen?', + end_soft: 'Zacht Stoppen', + enlarged_qr_code: 'Vergrote QR Code', + enter_password_to_confirm_delete_user: 'Voer uw wachtwoord in om de verwijdering van het account te bevestigen', + error_message_is_missing: 'Foutbericht ontbreekt.', + error_unknown_cause: 'Er is een onbekende fout opgetreden.', + error_uploading_files: 'Bestanden uploaden mislukt', + favorites: 'Favorieten', + feedback: 'Feedback', + feedback_c2a: 'Gebruik het onderstaande formulier om ons uw feedback, opmerkingen en bugrapporten te sturen.', + feedback_sent_confirmation: 'Bedankt dat u contact met ons heeft opgenomen. Als u een e-mail heeft gekoppeld aan uw account, hoort u zo snel mogelijk van ons.', + fit: 'Aanpassen', + folder: 'Map', + force_quit: 'Geforceerd Stoppen', + forgot_pass_c2a: 'Wachtwoord vergeten?', + from: 'Van', + general: 'Algemeen', + get_a_copy_of_on_puter: `Krijg een kopie van '%%' op Puter.com!`, + get_copy_link: 'Krijg Kopieerlink', + hide_all_windows: 'Alle Vensters Verbergen', + home: 'Home', html_document: 'HTML document', + hue: 'Tint', image: 'Afbeelding', - invite_link: "Uitnodiging Koppeling", - item: 'Bestand', - items_in_trash_cannot_be_renamed: `Dit bestand kan niet hernoemd worden omdat het in de Prullenbak zit. Om dit bestand te hernoemen, verplaats het eerst uit de Prullenbak.`, + incorrect_password: 'Onjuist wachtwoord', + invite_link: 'Uitnodigingslink', + item: 'item', + items_in_trash_cannot_be_renamed: `Dit item kan niet worden hernoemd omdat het in de prullenbak zit. Om dit item te hernoemen, sleept u het eerst uit de prullenbak.`, jpeg_image: 'JPEG afbeelding', - keep_in_taskbar: 'In Taakbalk houden', - language: "Taal", - license: "Licentie", + keep_in_taskbar: 'In Taakbalk Houden', + language: 'Taal', + license: 'Licentie', + lightness: 'Helderheid', + link_copied: 'Link gekopieerd', loading: 'Laden', - log_in: "Inloggen", - log_into_another_account_anyway: 'Alsnog met ander account inloggen', + log_in: 'Inloggen', + log_into_another_account_anyway: 'Log toch in op een ander account', log_out: 'Uitloggen', + looks_good: 'Ziet er goed uit!', + manage_sessions: 'Sessies Beheren', + menubar_style: 'Menubalk Stijl', + menubar_style_desktop: 'Bureaublad', + menubar_style_system: 'Systeem', + menubar_style_window: 'Venster', + mobile_device: 'Mobiel apparaat', + modified: 'Gewijzigd', move: 'Verplaatsen', - moving_file: "Aan het verplaatsen %%", - my_websites: "Mijn Websites", + moving_file: 'Verplaatsen %%', + my_websites: 'Mijn Websites', name: 'Naam', name_cannot_be_empty: 'Naam kan niet leeg zijn.', - name_cannot_contain_double_period: "Naam mag geen '..'-karakter bevatten.", - name_cannot_contain_period: "Naam mag geen '.'-karakter bevatten.", - name_cannot_contain_slash: "Naam mag geen '/'-karakter bevatten.", - name_must_be_string: "Naam kan alleen een tekenreeks zijn.", - name_too_long: `Naam kan niet langer zijn dan %% karakters.`, + name_cannot_contain_double_period: 'Naam mag geen dubbele punt \'..\' bevatten.', + name_cannot_contain_period: 'Naam mag geen punt \' . \' bevatten.', + name_cannot_contain_slash: 'Naam mag geen schuine streep \' / \' bevatten.', + name_must_be_string: 'Naam moet een alfanumerieke tekenreeks zijn.', + name_too_long: 'Naam mag niet langer zijn dan %% karakters.', new: 'Nieuw', + new_email: 'Nieuwe E-mail', new_folder: 'Nieuwe Map', - new_password: "Nieuw Wachtwoord", - new_username: "Nieuwe Gebruikersnaam", + new_password: 'Nieuw Wachtwoord', + new_username: 'Nieuwe Gebruikersnaam', no: 'Nee', - no_dir_associated_with_site: 'Er is geen map gekoppeld aan dit adres.', - no_websites_published: "U heeft nog geen websites gepubliceerd.", - ok: 'Oke', - open: "Open", - open_in_new_tab: "Open in Nieuw Tabblad", - open_in_new_window: "Open in Nieuw Scherm", - open_with: "Openen met", - oss_code_and_content: "Open source-software en inhoud", - password: "Wachtwoord", - password_changed: "Wachtwoord veranderd.", - passwords_do_not_match: '`Nieuw Wachtwoord` en `Bevestig Nieuw Wachtwoord` komen niet overeen.', + no_dir_associated_with_site: 'Geen map geassocieerd met dit adres.', + no_websites_published: 'Je hebt nog geen websites gepubliceerd. Klik met de rechtermuisknop op een map om te beginnen.', + ok: 'OK', + open: 'Openen', + open_in_new_tab: 'Openen in Nieuw Tabblad', + open_in_new_window: 'Openen in Nieuw Venster', + open_with: 'Openen met', + original_name: 'Originele Naam', + original_path: 'Origineel Pad', + oss_code_and_content: 'Open Source Software Code en Inhoud', + password: 'Wachtwoord', + password_changed: 'Wachtwoord gewijzigd', + password_recovery_rate_limit: 'U heeft te veel verzoeken ingediend. Wacht een paar minuten voordat u het opnieuw probeert.', + password_recovery_token_invalid: 'Wachtwoord hersteltoken is verlopen of ongeldig.', + password_recovery_unknown_error: 'Onbekende fout opgetreden bij het herstellen van het wachtwoord. Probeer het opnieuw.', + password_required: 'Wachtwoord is verplicht.', + password_strength_error: 'Wachtwoord moet minimaal 8 tekens lang zijn en minimaal één hoofdletter, één kleine letter, één cijfer en één speciaal teken bevatten.', + passwords_do_not_match: 'Wachtwoorden komen niet overeen.', paste: 'Plakken', - paste_into_folder: "Plakken in Map", - pick_name_for_website: "Kies een naam voor uw website:", - picture: "Foto", - powered_by_puter_js: `Aangedreven door {{link=docs}}Puter.js{{/link}}`, - preparing: "Voorbereiden...", - preparing_for_upload: "Upload voorbereiden...", - proceed_to_login: 'Doorgaan naar Inloggen', - properties: "Eigenschappen", - privacy: "Privacy", - proceed_with_account_deletion: "Doorgaan met Account Verwijderen", - publish: "Publiceren", - publish_as_website: 'Publiceren als website', - puter_description: `Puter is een persoonlijke cloud waarin privacy centraal staat en waarmee u al uw bestanden, apps en games in één veilige plek kunt bewaren die altijd en overal toegankelijk.`, - recent: "Recent", - recover_password: "Wachtwoord herstellen", - refer_friends_c2a: "Krijg 1 GB voor elke vriend die een account maakt en bevestigt bij Puter. uw vriend ontvangt ook 1 GB!", - refer_friends_social_media_c2a: `Krijg 1 GB gratig opslag op Puter.com!`, - refresh: 'Verversen', - release_address_confirmation: `Weet u zeker dat u dit adres wilt vrijgeven?`, - remove_from_taskbar:'Losmaken van Taakbalk', + paste_into_folder: 'Plakken in Map', + path: 'Pad', + personalization: 'Personalisatie', + pick_name_for_website: 'Kies een naam voor uw website', + picture: 'Afbeelding', + pictures: 'Afbeeldingen', + plural_suffix: 'en', + powered_by_puter_js: 'Aangedreven door {{link=docs}}Puter.js{{/link}}', + preparing: 'Voorbereiden...', + preparing_for_upload: 'Voorbereiden voor upload...', + print: 'Afdrukken', + privacy: 'Privacy', + proceed_to_login: 'Doorgaan naar inloggen', + proceed_with_account_deletion: 'Doorgaan met accountverwijdering', + process_status_initializing: 'Initialiseren', + process_status_running: 'Bezig', + process_type_app: 'App', + process_type_init: 'Init', + process_type_ui: 'UI', + properties: 'Eigenschappen', + public: 'Openbaar', + publish: 'Publiceren', + publish_as_website: 'Publiceer als website', + puter_description: `Puter is een privacy-first persoonlijke cloud om al je bestanden, apps en games op één veilige plek te bewaren, overal en altijd toegankelijk.`, + reading_file: 'Lezen %strong%', + recent: 'Recent', + recommended: 'Aanbevolen', + recover_password: 'Wachtwoord herstellen', + refer_friends_c2a: 'Krijg 1 GB voor elke vriend die een account aanmaakt en bevestigt op Puter. Je vriend krijgt ook 1 GB!', + refer_friends_social_media_c2a: `Krijg 1 GB gratis opslagruimte op Puter.com!`, + refresh: 'Vernieuwen', + release_address_confirmation: `Weet je zeker dat je dit adres wilt vrijgeven?`, + remove_from_taskbar: 'Verwijderen van taakbalk', rename: 'Hernoemen', repeat: 'Herhalen', replace: 'Vervangen', - replace_all: 'Alles Vervangen', - resend_confirmation_code: "Confirmatie Code opnieuw verzenden", - restore: "Herstellen", + replace_all: 'Alles vervangen', + resend_confirmation_code: 'Bevestigingscode opnieuw verzenden', + reset_colors: 'Kleuren resetten', + restart_puter_confirm: 'Weet je zeker dat je Puter wilt herstarten?', + restore: 'Herstellen', + save: 'Opslaan', + saturation: 'Verzadiging', save_account: 'Account opslaan', - save_account_to_get_copy_link: "Maak alstublieft een account om door te gaan.", - save_account_to_publish: 'Maak alstublieft een account om door te gaan.', - save_session: 'Sessie Opslaan', - save_session_c2a: 'Maak een account aan om uw huidige sessie op te slaan en te voorkomen dat u uw werk kwijtraakt.', - scan_qr_c2a: 'Scan de onderstaande QR-code om op deze sessie in te loggen op een ander apparaat', - select: "Selecteren", + save_account_to_get_copy_link: 'Maak een account aan om verder te gaan.', + save_account_to_publish: 'Maak een account aan om verder te gaan.', + save_session: 'Sessie opslaan', + save_session_c2a: 'Maak een account aan om je huidige sessie op te slaan en verlies van werk te voorkomen.', + scan_qr_c2a: 'Scan de onderstaande code\nom in te loggen op deze sessie vanaf andere apparaten', + scan_qr_2fa: 'Scan de QR-code met je authenticator-app', + scan_qr_generic: 'Scan deze QR-code met je telefoon of een ander apparaat', + search: 'Zoeken', + seconds: 'seconden', + security: 'Beveiliging', + select: 'Selecteren', selected: 'geselecteerd', select_color: 'Selecteer kleur…', - send: "Versturen", - send_password_recovery_email: "Verstuur Wachtwoord Herstel Email", - session_saved: "Bedankt voor het maken van een account. Deze sessie is nu opgeslagen.", + sessions: 'Sessies', + send: 'Verzenden', + send_password_recovery_email: 'Wachtwoordherstelsmail verzenden', + session_saved: 'Bedankt voor het aanmaken van een account. Deze sessie is opgeslagen.', settings: 'Instellingen', - set_new_password: "Stel een nieuw wachtwoord in", - share_to: "Delen naar", - show_all_windows: "Toon alle vensters", - show_hidden: 'Toon Verborgen', - sign_in_with_puter: "Inloggen met Puter", - sign_up: "Aanmelden", - signing_in: "Aan het inloggen…", + set_new_password: 'Nieuw wachtwoord instellen', + share: 'Delen', + share_to: 'Delen met', + share_with: 'Delen met:', + shortcut_to: 'Snelkoppeling naar', + show_all_windows: 'Toon alle vensters', + show_hidden: 'Toon verborgen', + sign_in_with_puter: 'Inloggen met Puter', + sign_up: 'Aanmelden', + signing_in: 'Inloggen…', size: 'Grootte', skip: 'Overslaan', + something_went_wrong: 'Er is iets misgegaan.', sort_by: 'Sorteren op', start: 'Start', - status: "Status", - storage_usage: "Opslaggebruik", - taking_longer_than_usual: 'Het duurt iets langer dan normaal. Even geduld aub...', - terms: "Voorwaarden", - text_document: 'Tekst document', - tos_fineprint: `Door te klikken op 'Maak Gratis Account' gaat u akkoord met Puter's {{link=terms}}Gebruiksvoorwaarden{{/link}} en {{link=privacy}}Privacybeleid{{/link}}.`, + status: 'Status', + storage_usage: 'Opslaggebruik', + storage_puter_used: 'gebruikt door Puter', + taking_longer_than_usual: 'Het duurt langer dan normaal. Even geduld alstublieft...', + task_manager: 'Taakbeheer', + taskmgr_header_name: 'Naam', + taskmgr_header_status: 'Status', + taskmgr_header_type: 'Type', + terms: 'Voorwaarden', + text_document: 'Tekstdocument', + tos_fineprint: `Door op 'Gratis Account Aanmaken' te klikken, ga je akkoord met de {{link=terms}}Servicevoorwaarden{{/link}} en de {{link=privacy}}Privacybeleid{{/link}} van Puter.`, + transparency: 'Transparantie', trash: 'Prullenbak', + two_factor: 'Tweestapsverificatie', + two_factor_disabled: '2FA Uitgeschakeld', + two_factor_enabled: '2FA Ingeschakeld', type: 'Type', - type_confirm_to_delete_account: "Type 'bevestig' om uw account te verwijderen.", + type_confirm_to_delete_account: 'Typ \'bevestigen\' om je account te verwijderen.', + ui_colors: 'UI Kleuren', + ui_manage_sessions: 'Sessiebeheer', + ui_revoke: 'Intrekken', undo: 'Ongedaan maken', unlimited: 'Onbeperkt', - unzip: "Uitpakken", + unzip: 'Uitpakken', upload: 'Uploaden', - upload_here: 'Hier Uploaden', + upload_here: 'Hier uploaden', usage: 'Gebruik', - username: "Gebruikersnaam", - username_changed: 'Gebruikersnaam succesvol aangepast.', - versions: "Versies", + username: 'Gebruikersnaam', + username_changed: 'Gebruikersnaam succesvol bijgewerkt.', + username_required: 'Gebruikersnaam is vereist.', + versions: 'Versies', + videos: 'Video\'s', + visibility: 'Zichtbaarheid', yes: 'Ja', - yes_release_it: 'Ja, Geef het vrij', - you_have_been_referred_to_puter_by_a_friend: "Je bent door een vriend naar Puter verwezen!", - zip: "Inpakken", + yes_release_it: 'Ja, vrijgeven', + you_have_been_referred_to_puter_by_a_friend: 'Je bent door een vriend doorverwezen naar Puter!', + zip: 'Zip', + zipping_file: 'Bestand inpakken %strong%', + + // === 2FA Setup === + setup2fa_1_step_heading: 'Open je authenticator-app', + setup2fa_1_instructions: ` + Je kunt elke authenticator-app gebruiken die het Time-based One-Time Password (TOTP) protocol ondersteunt. + Er zijn veel opties om uit te kiezen, maar als je twijfelt, + Authy + is een goede keuze voor Android en iOS. + `, + setup2fa_2_step_heading: 'Scan de QR-code', + setup2fa_3_step_heading: 'Voer de 6-cijferige code in', + setup2fa_4_step_heading: 'Kopieer je herstelcodes', + setup2fa_4_instructions: ` + Deze herstelcodes zijn de enige manier om toegang te krijgen tot je account als je je telefoon kwijtraakt of je authenticator-app niet kunt gebruiken. + Zorg ervoor dat je ze op een veilige plaats bewaart. + `, + setup2fa_5_step_heading: 'Bevestig 2FA-instelling', + setup2fa_5_confirmation_1: 'Ik heb mijn herstelcodes op een veilige plaats opgeslagen', + setup2fa_5_confirmation_2: 'Ik ben klaar om 2FA in te schakelen', + setup2fa_5_button: '2FA inschakelen', + + // === 2FA Login === + login2fa_otp_title: 'Voer 2FA-code in', + login2fa_otp_instructions: 'Voer de 6-cijferige code in uit je authenticator-app.', + login2fa_recovery_title: 'Voer een herstelcode in', + login2fa_recovery_instructions: 'Voer een van je herstelcodes in om toegang te krijgen tot je account.', + login2fa_use_recovery_code: 'Gebruik een herstelcode', + login2fa_recovery_back: 'Terug', + login2fa_recovery_placeholder: 'XXXXXXXX', } }; diff --git a/tools/run-selfhosted.js b/tools/run-selfhosted.js index a659656a1..ec2985bca 100644 --- a/tools/run-selfhosted.js +++ b/tools/run-selfhosted.js @@ -83,7 +83,8 @@ const main = async () => { CoreModule, DatabaseModule, LocalDiskStorageModule, - SelfHostedModule + SelfHostedModule, + TestDriversModule, } = (await import('@heyputer/backend')).default; const k = new Kernel({ @@ -93,6 +94,7 @@ const main = async () => { k.add_module(new DatabaseModule()); k.add_module(new LocalDiskStorageModule()); k.add_module(new SelfHostedModule()); + k.add_module(new TestDriversModule()); k.boot(); };