community: validate the url in the dialog

This commit is contained in:
Girish Ramakrishnan
2026-02-05 21:51:55 +01:00
parent 13b524e8a5
commit aa362477e8
5 changed files with 67 additions and 78 deletions
+11 -36
View File
@@ -7,8 +7,6 @@ import { prettyDate, prettyBinarySize } from '@cloudron/pankow/utils';
import AccessControl from './AccessControl.vue';
import PortBindings from './PortBindings.vue';
import AppsModel from '../models/AppsModel.js';
import CommunityModel from '../models/CommunityModel.js';
import AppstoreModel from '../models/AppstoreModel.js';
import DomainsModel from '../models/DomainsModel.js';
import UsersModel from '../models/UsersModel.js';
import GroupsModel from '../models/GroupsModel.js';
@@ -20,9 +18,7 @@ const STEP = Object.freeze({
INSTALL: Symbol('install'),
});
const appstoreModel = AppstoreModel.create();
const appsModel = AppsModel.create();
const communityModel = CommunityModel.create();
const domainsModel = DomainsModel.create();
const usersModel = UsersModel.create();
const groupsModel = GroupsModel.create();
@@ -33,7 +29,7 @@ const dashboardDomain = inject('dashboardDomain');
// reactive
const busy = ref(false);
const formError = ref({});
const appData = ref({});
const packageData = ref({});
const manifest = ref({});
const step = ref(STEP.DETAILS);
const dialog = useTemplateRef('dialogHandle');
@@ -159,7 +155,7 @@ async function onSubmit(overwriteDns) {
if (manifest.value.id === PROXY_APP_ID) config.upstreamUri = upstreamUri.value;
const [error, result] = await appsModel.install(appData.value, config);
const [error, result] = await appsModel.install(packageData.value, config);
if (!error) {
dialog.value.close();
@@ -213,36 +209,14 @@ function onScreenshotNext() {
}
defineExpose({
open: async function(upstreamRef, appCountExceeded, domainList) {
open: async function(pd, appCountExceeded, domainList) {
busy.value = false;
step.value = STEP.LOADING;
formError.value = {};
// give it some time to fetch before showing loading
const openTimer = setTimeout(dialog.value.open, 200);
if (upstreamRef.appStoreId) {
const [id, version] = upstreamRef.appStoreId.split('@');
const [error, result] = await appstoreModel.get(id, version);
if (error) {
clearTimeout(openTimer);
dialog.value.close();
throw new Error('App not found');
}
appData.value = { ...result, ...upstreamRef };
} else if (upstreamRef.versionsUrl) {
const [url, version] = upstreamRef.versionsUrl.split('@');
const [error, result] = await communityModel.getApp(url, version);
if (error) {
clearTimeout(openTimer);
dialog.value.close();
throw new Error(error.body?.message || 'Failed to fetch community app');
}
appData.value = { ...result, ...upstreamRef };
}
packageData.value = pd;
appMaxCountExceeded.value = appCountExceeded;
manifest.value = appData.value.manifest;
manifest.value = packageData.value.manifest;
location.value = '';
accessRestrictionOption.value = ACL_OPTIONS.ANY;
accessRestrictionAcl.value = { users: [], groups: [] };
@@ -281,6 +255,7 @@ defineExpose({
currentScreenshotPos = 0;
step.value = STEP.DETAILS;
dialog.value.open();
},
close() {
dialog.value.close();
@@ -296,13 +271,13 @@ defineExpose({
</div>
<div v-else class="app-install-dialog-body" :class="{ 'step-detail': step === STEP.DETAILS, 'step-install': step === STEP.INSTALL }">
<div class="app-install-header">
<div class="summary" v-if="appData.manifest">
<div class="title"><img class="icon pankow-no-desktop" style="width: 32px; height: 32px; margin-right: 10px" :src="appData.iconUrl" />{{ manifest.title }}</div>
<div><a :href="manifest.website" target="_blank">{{ manifest.title }}</a> {{ appData.manifest.upstreamVersion }} - {{ $t('app.updates.info.packageVersion') }} {{ appData.manifest.version }}</div>
<div>{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(appData.creationDate) }) }}</div>
<div class="summary" v-if="packageData.manifest">
<div class="title"><img class="icon pankow-no-desktop" style="width: 32px; height: 32px; margin-right: 10px" :src="packageData.iconUrl" />{{ manifest.title }}</div>
<div><a :href="manifest.website" target="_blank">{{ manifest.title }}</a> {{ packageData.manifest.upstreamVersion }} - {{ $t('app.updates.info.packageVersion') }} {{ packageData.manifest.version }}</div>
<div>{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(packageData.creationDate) }) }}</div>
<div>{{ $t('appstore.installDialog.memoryRequirement', { size: prettyBinarySize(manifest.memoryLimit || (256 * 1024 * 1024)) }) }}</div>
</div>
<img class="icon pankow-no-mobile" :src="appData.iconUrl" />
<img class="icon pankow-no-mobile" :src="packageData.iconUrl" />
</div>
<Transition name="slide-left" mode="out-in">
<div v-if="step === STEP.DETAILS">
+40 -25
View File
@@ -2,31 +2,48 @@
import { ref, useTemplateRef } from 'vue';
import { Dialog, TextInput, FormGroup } from '@cloudron/pankow';
import CommunityModel from '../models/CommunityModel.js';
const communityModel = CommunityModel.create();
const emit = defineEmits([ 'success' ]);
const dialog = useTemplateRef('dialog');
const form = useTemplateRef('form');
const formError = ref({});
const url = ref('');
const version = ref('latest');
const versionsUrl = ref('');
const busy = ref(false);
function onSubmit() {
const isFormValid = ref(false);
function validateForm() {
isFormValid.value = form.value ? form.value.checkValidity() : false;
}
async function onSubmit() {
if (!form.value.reportValidity()) return;
busy.value = true;
formError.value = {};
if (!url.value || !version.value) {
formError.value.generic = 'URL and version are required';
return;
const [error, result] = await communityModel.getApp(versionsUrl.value, 'latest');
if (error) {
formError.value.generic = error.body ? error.body.message : 'Internal error';
busy.value = false;
return console.error(error);
}
emit('success', { url: url.value, version: version.value });
const packageData = { ...result, versionsUrl: versionsUrl.value + '@latest' };
emit('success', packageData);
dialog.value.close();
busy.value = false;
}
defineExpose({
open() {
url.value = '';
version.value = 'latest';
versionsUrl.value = '';
formError.value = {};
dialog.value.open();
}
@@ -37,26 +54,24 @@ defineExpose({
<template>
<Dialog ref="dialog"
title="Install Community App"
confirm-label="Continue"
:confirm-active="url !== ''"
:confirm-label="$t('main.action.add')"
:confirm-busy="busy"
:confirm-active="!busy && isFormValid"
reject-style="secondary"
reject-label="Cancel"
:reject-label="$t('main.dialog.cancel')"
:reject-active="!busy"
@confirm="onSubmit()"
>
<form @submit.prevent="onSubmit()" autocomplete="off">
<input type="submit" style="display: none;" />
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form" @input="validateForm()">
<fieldset :disabled="busy">
<input type="submit" style="display: none;" />
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
<FormGroup>
<label for="urlInput">CloudronVersions.json URL</label>
<TextInput id="urlInput" v-model="url" required placeholder="https://example.com/CloudronVersions.json"/>
</FormGroup>
<FormGroup>
<label for="versionInput">Version</label>
<TextInput id="versionInput" v-model="version" required/>
</FormGroup>
<FormGroup>
<label for="urlInput">CloudronVersions.json URL</label>
<TextInput id="urlInput" v-model="versionsUrl" required placeholder="https://example.com/CloudronVersions.json"/>
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
</FormGroup>
</fieldset>
</form>
</Dialog>
</template>
+13 -14
View File
@@ -144,17 +144,18 @@ async function onHashChange() {
const params = new URLSearchParams(window.location.hash.slice(window.location.hash.indexOf('?')));
const version = params.get('version') || 'latest';
// const [error, appData] = await appstoreModel.get(appId, version);
// if (error) {
// console.error(error);
// return inputDialog.value.info({
// title: t('appstore.appNotFoundDialog.title'),
// message: t('appstore.appNotFoundDialog.description', { appId, version }),
// confirmLabel: t('main.dialog.close'),
// });
// }
const [error, result] = await appstoreModel.get(appId, version);
if (error) {
console.error(error);
return inputDialog.value.info({
title: t('appstore.appNotFoundDialog.title'),
message: t('appstore.appNotFoundDialog.description', { appId, version }),
confirmLabel: t('main.dialog.close'),
});
}
appInstallDialog.value.open({ appStoreId: `${appId}@${version}` }, installedApps.value.length >= features.value.appMaxCount, domains.value);
const packageData = { ...result, appStoreId: `${appId}@${version}` };
appInstallDialog.value.open(packageData, installedApps.value.length >= features.value.appMaxCount, domains.value);
}
}
@@ -194,10 +195,8 @@ function onInstallCommunityApp() {
communityAppDialog.value.open();
}
function onCommunityAppSuccess({ url, version }) {
// Construct versionsUrl in url@version format
const versionsUrl = `${url}@${version}`;
appInstallDialog.value.open({ versionsUrl }, installedApps.value.length >= features.value.appMaxCount, domains.value);
function onCommunityAppSuccess(packageData) {
appInstallDialog.value.open(packageData, installedApps.value.length >= features.value.appMaxCount, domains.value);
}
onActivated(async () => {