diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 1562093..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - root: true, - parser: 'vue-eslint-parser', - parserOptions: { - parser: '@babel/eslint-parser', - ecmaVersion: 2018, - sourceType: 'module' - }, - extends: [ - 'plugin:vue/recommended', - 'standard' - ], - rules: { - 'vue/max-attributes-per-line': 'off' - } -} diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml index 6d24a2b..deaff2d 100644 --- a/.github/workflows/laravel.yml +++ b/.github/workflows/laravel.yml @@ -86,13 +86,6 @@ jobs: restore-keys: | ${{ runner.os }}-composer- - - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -128,6 +121,31 @@ jobs: path: storage/logs/laravel.log retention-days: 3 + build-nuxt-app: + runs-on: ubuntu-latest + name: Build the Nuxt app + defaults: + run: + working-directory: ./client + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: '20' + + - uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Prepare the environment + run: cp .env.example .env + - name: Install npm dependencies run: npm install --no-audit --no-progress --silent diff --git a/README.md b/README.md index ec84b07..087db45 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ It takes 1 minute to try out the builder for free. You'll have high availability ### Docker installation 🐳 -There's a `Dockerfile` for building a self-contained docker image including databases, webservers etc. +> ⚠️ **Warning**: the Docker setup is currently not working as we're migrating the front-end to Nuxt. [Track progress here](https://github.com/JhumanJ/OpnForm/issues/283). This can be built and run locally but is also hosted publicly on docker hub at `jhumanj/opnform` and is generally best run directly from there. @@ -154,8 +154,11 @@ First, let's work with the codebase and its dependencies. # Get the code! git clone git@github.com:JhumanJ/OpnForm.git && cd OpnForm -# Install PHP and JS dependencies -composer install && npm install +# Install PHP dependencies +composer install + + # Install JS dependencies +cd client && npm install # Compile assets (see the scripts section in package.json) npm run dev # or build @@ -186,7 +189,8 @@ Now, create an S3 bucket (or equivalent). Create an IAM user with access to this OpnForm is a standard web application built with: - [Laravel](https://laravel.com/) PHP framework -- [Vue.js](https://vuejs.org/) front-end framework +- [NuxtJs](https://nuxt.com/) Front-end SSR framework +- [Vue.js 3](https://vuejs.org/) Front-end framework - [TailwindCSS](https://tailwindcss.com/) ## Contribute diff --git a/amplify.yml b/amplify.yml new file mode 100644 index 0000000..5b6affd --- /dev/null +++ b/amplify.yml @@ -0,0 +1,17 @@ +version: 1 +frontend: + phases: + preBuild: + commands: + - cd client + - npm ci + build: + commands: + - npm run build + artifacts: + baseDirectory: client/.amplify-hosting + files: + - '**/*' + cache: + paths: + - client/node_modules/**/* diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 830207d..966ea02 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -48,7 +48,7 @@ class Handler extends ExceptionHandler { return $request->expectsJson() ? response()->json(['message' => $exception->getMessage()], 401) - : redirect()->guest(url('/login')); + : redirect(front_url('login')); } public function report(Throwable $exception) diff --git a/app/Http/Controllers/Auth/AppSumoAuthController.php b/app/Http/Controllers/Auth/AppSumoAuthController.php index 505d2d9..a5cd5aa 100644 --- a/app/Http/Controllers/Auth/AppSumoAuthController.php +++ b/app/Http/Controllers/Auth/AppSumoAuthController.php @@ -28,10 +28,10 @@ class AppSumoAuthController extends Controller // otherwise start login flow by passing the encrypted license key id if (is_null($license->user_id)) { - return redirect(url('/register?appsumo_license='.encrypt($license->id))); + return redirect(front_url('/register?appsumo_license='.encrypt($license->id))); } - return redirect(url('/register?appsumo_error=1')); + return redirect(front_url('/register?appsumo_error=1')); } private function retrieveAccessToken(string $requestCode): string @@ -82,11 +82,11 @@ class AppSumoAuthController extends Controller if (is_null($license->user_id)) { $license->user_id = Auth::id(); $license->save(); - return redirect(url('/home?appsumo_connect=1')); + return redirect(front_url('/home?appsumo_connect=1')); } // Licensed already attached - return redirect(url('/home?appsumo_error=1')); + return redirect(front_url('/home?appsumo_error=1')); } /** diff --git a/app/Http/Controllers/Content/FileUploadController.php b/app/Http/Controllers/Content/FileUploadController.php index 62e6423..3481d8c 100644 --- a/app/Http/Controllers/Content/FileUploadController.php +++ b/app/Http/Controllers/Content/FileUploadController.php @@ -17,6 +17,7 @@ class FileUploadController extends Controller */ public function upload(Request $request) { + $request->validate(['file' => 'required|file']); $uuid = (string) Str::uuid(); $path = $request->file('file')->storeAs(PublicFormController::TMP_FILE_UPLOAD_PATH, $uuid); diff --git a/app/Http/Controllers/Forms/FormSubmissionController.php b/app/Http/Controllers/Forms/FormSubmissionController.php index 75d1a90..dca04b1 100644 --- a/app/Http/Controllers/Forms/FormSubmissionController.php +++ b/app/Http/Controllers/Forms/FormSubmissionController.php @@ -15,7 +15,8 @@ class FormSubmissionController extends Controller { public function __construct() { - $this->middleware('auth'); + $this->middleware('auth', ['except' => ['submissionFile']]); + $this->middleware('signed', ['only' => ['submissionFile']]); } public function submissions(string $id) @@ -51,9 +52,6 @@ class FormSubmissionController extends Controller public function submissionFile($id, $fileName) { - $form = Form::findOrFail((int) $id); - $this->authorize('view', $form); - $fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id).'/' .urldecode($fileName); @@ -63,8 +61,12 @@ class FormSubmissionController extends Controller ], 404); } + if (config('filesystems.default') !== 's3') { + return response()->file(Storage::path($fileName)); + } + return redirect( - Storage::temporaryUrl($fileName, now()->addMinute()) + Storage::temporaryUrl($fileName, now()->addMinute()) ); } } diff --git a/app/Http/Controllers/SitemapController.php b/app/Http/Controllers/SitemapController.php index 1c624e0..712073e 100644 --- a/app/Http/Controllers/SitemapController.php +++ b/app/Http/Controllers/SitemapController.php @@ -9,62 +9,24 @@ use App\Models\Template; class SitemapController extends Controller { - /** - * Contains route name and the associated priority - * - * @var array - */ - protected $urls = [ - ['/', 1], - ['/pricing', 0.9], - ['/privacy-policy', 0.5], - ['/terms-conditions', 0.5], - ['/login', 0.4], - ['/register', 0.4], - ['/password/reset', 0.3], - ['/form-templates', 0.9], - ]; - public function getSitemap(Request $request) + public function index(Request $request) { - $sitemap = Sitemap::create(); - foreach ($this->urls as $url) { - $sitemap->add($this->createUrl($url[0], $url[1])); - } - $this->addTemplatesUrls($sitemap); - $this->addTemplatesTypesUrls($sitemap); - $this->addTemplatesIndustriesUrls($sitemap); - - return $sitemap->toResponse($request); + return [ + ...$this->getTemplatesUrls() + ]; } - private function createUrl($url, $priority, $frequency = 'daily') + private function getTemplatesUrls() { - return Url::create($url)->setPriority($priority)->setChangeFrequency($frequency); - } - - private function addTemplatesUrls(Sitemap $sitemap) - { - Template::where('publicly_listed', true)->chunk(100, function ($templates) use ($sitemap) { + $urls = []; + Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) { foreach ($templates as $template) { - $sitemap->add($this->createUrl('/form-templates/' . $template->slug, 0.8)); + $urls[] = [ + 'loc' => '/templates/' . $template->slug + ]; } }); - } - - private function addTemplatesTypesUrls(Sitemap $sitemap) - { - $types = json_decode(file_get_contents(resource_path('data/forms/templates/types.json')), true); - foreach ($types as $type) { - $sitemap->add($this->createUrl('/form-templates/types/' . $type['slug'], 0.7)); - } - } - - private function addTemplatesIndustriesUrls(Sitemap $sitemap) - { - $industries = json_decode(file_get_contents(resource_path('data/forms/templates/industries.json')), true); - foreach ($industries as $industry) { - $sitemap->add($this->createUrl('/form-templates/industries/' . $industry['slug'], 0.7)); - } + return $urls; } } diff --git a/app/Http/Controllers/SpaController.php b/app/Http/Controllers/SpaController.php deleted file mode 100644 index 50456f1..0000000 --- a/app/Http/Controllers/SpaController.php +++ /dev/null @@ -1,18 +0,0 @@ - (new SeoMetaResolver($request))->getMetas(), - ]); - } -} diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index 4205b36..8cc0213 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -45,8 +45,8 @@ class SubscriptionController extends Controller $checkout = $checkoutBuilder ->collectTaxIds() ->checkout([ - 'success_url' => url('/subscriptions/success'), - 'cancel_url' => url('/subscriptions/error'), + 'success_url' => front_url('/subscriptions/success'), + 'cancel_url' => front_url('/subscriptions/error'), 'billing_address_collection' => 'required', 'customer_update' => [ 'address' => 'auto', diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 556e49c..1ae7aeb 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -4,7 +4,6 @@ namespace App\Http; use App\Http\Middleware\AuthenticateJWT; use App\Http\Middleware\CustomDomainRestriction; -use App\Http\Middleware\EmbeddableForms; use App\Http\Middleware\IsAdmin; use App\Http\Middleware\IsNotSubscribed; use App\Http\Middleware\IsSubscribed; @@ -20,9 +19,9 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ - // \App\Http\Middleware\TrustHosts::class, +// \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, - \Fruitcake\Cors\HandleCors::class, + \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, @@ -46,16 +45,14 @@ class Kernel extends HttpKernel \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, - EmbeddableForms::class ], 'spa' => [ \Illuminate\Routing\Middleware\SubstituteBindings::class, - EmbeddableForms::class ], 'api' => [ - 'throttle:60,1', + 'throttle:100,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\EncryptCookies::class, \Illuminate\Session\Middleware\StartSession::class, diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 513b77e..b481f9f 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -15,7 +15,7 @@ class Authenticate extends Middleware protected function redirectTo($request) { if (! $request->expectsJson()) { - return redirect('/login'); + return redirect(front_url('login')); } } } diff --git a/app/Http/Middleware/AuthenticateJWT.php b/app/Http/Middleware/AuthenticateJWT.php index 7fc10ef..8cbc86b 100644 --- a/app/Http/Middleware/AuthenticateJWT.php +++ b/app/Http/Middleware/AuthenticateJWT.php @@ -8,6 +8,7 @@ use Tymon\JWTAuth\Exceptions\JWTException; class AuthenticateJWT { + const API_SERVER_SECRET_HEADER_NAME = 'x-api-secret'; /** * Verifies the JWT token and validates the IP and User Agent @@ -24,6 +25,13 @@ class AuthenticateJWT // Validate IP and User Agent if ($payload) { + if ($frontApiSecret = $request->header(self::API_SERVER_SECRET_HEADER_NAME)) { + // If it's a trusted SSR request, skip the rest + if ($frontApiSecret === config('app.front_api_secret')) { + return $next($request); + } + } + $error = null; if (!\Hash::check($request->ip(), $payload->get('ip'))) { $error = 'Origin IP is invalid'; diff --git a/app/Http/Middleware/CustomDomainRestriction.php b/app/Http/Middleware/CustomDomainRestriction.php index 9553f27..31f4b0e 100644 --- a/app/Http/Middleware/CustomDomainRestriction.php +++ b/app/Http/Middleware/CustomDomainRestriction.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Builder; class CustomDomainRestriction { - const CUSTOM_DOMAIN_HEADER = "User-Custom-Domain"; + const CUSTOM_DOMAIN_HEADER = "x-custom-domain"; /** * Handle an incoming request. @@ -27,7 +27,8 @@ class CustomDomainRestriction return response()->json([ 'success' => false, 'message' => 'Invalid domain', - ], 400); + 'error' => 'invalid_domain', + ], 420); } // Check if domain is different from current domain @@ -41,7 +42,8 @@ class CustomDomainRestriction return response()->json([ 'success' => false, 'message' => 'Unknown domain', - ], 400); + 'error' => 'invalid_domain', + ], 420); } Workspace::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspace) { diff --git a/app/Http/Middleware/EmbeddableForms.php b/app/Http/Middleware/EmbeddableForms.php deleted file mode 100644 index e571fc4..0000000 --- a/app/Http/Middleware/EmbeddableForms.php +++ /dev/null @@ -1,36 +0,0 @@ -expectsJson() || $request->wantsJson()) { - return $next($request); - } - - $response = $next($request); - - if (!str_starts_with($request->url(), url('/forms/'))) { - if ($response instanceof Response) { - $response->header('X-Frame-Options', 'SAMEORIGIN'); - } elseif ($response instanceof \Symfony\Component\HttpFoundation\Response) { - $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); - } - } - - return $response; - } -} diff --git a/app/Http/Resources/FormSubmissionResource.php b/app/Http/Resources/FormSubmissionResource.php index 6018e9e..ad2b932 100644 --- a/app/Http/Resources/FormSubmissionResource.php +++ b/app/Http/Resources/FormSubmissionResource.php @@ -50,7 +50,11 @@ class FormSubmissionResource extends JsonResource return $file !== null && $file; })->map(function ($file) { return [ - 'file_url' => route('open.forms.submissions.file', [$this->form_id, $file]), + 'file_url' => \URL::signedRoute( + 'open.forms.submissions.file', + [$this->form_id, $file], + now()->addMinutes(10) + ), 'file_name' => $file, ]; }); diff --git a/app/Jobs/Form/StoreFormSubmissionJob.php b/app/Jobs/Form/StoreFormSubmissionJob.php index 8361f04..198bb2c 100644 --- a/app/Jobs/Form/StoreFormSubmissionJob.php +++ b/app/Jobs/Form/StoreFormSubmissionJob.php @@ -164,14 +164,14 @@ class StoreFormSubmissionJob implements ShouldQueue return null; } - if(filter_var($value, FILTER_VALIDATE_URL) !== FALSE && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3 + if(filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3 $fileName = basename($value); $path = FormController::ASSETS_UPLOAD_PATH . '/' . $fileName; $newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id); Storage::move($path, $newPath.'/'.$fileName); return $fileName; } - + if($this->isSkipForUpload($value)) { return $value; } diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php index edfea0d..53c4039 100644 --- a/app/Models/Forms/Form.php +++ b/app/Models/Forms/Form.php @@ -157,12 +157,12 @@ class Form extends Model implements CachableAttributes if ($this->custom_domain) { return 'https://' . $this->custom_domain . '/forms/' . $this->slug; } - return url('/forms/' . $this->slug); + return front_url('/forms/' . $this->slug); } public function getEditUrlAttribute() { - return url('/forms/' . $this->slug . '/show'); + return front_url('/forms/' . $this->slug . '/show'); } public function getSubmissionsCountAttribute() diff --git a/app/Models/Template.php b/app/Models/Template.php index 59c42e3..45fa38f 100644 --- a/app/Models/Template.php +++ b/app/Models/Template.php @@ -48,7 +48,7 @@ class Template extends Model public function getShareUrlAttribute() { - return url('/form-templates/'.$this->slug); + return front_url('/form-templates/'.$this->slug); } public function setDescriptionAttribute($value) diff --git a/app/Notifications/ResetPassword.php b/app/Notifications/ResetPassword.php index 9edd12f..5260531 100644 --- a/app/Notifications/ResetPassword.php +++ b/app/Notifications/ResetPassword.php @@ -17,7 +17,7 @@ class ResetPassword extends Notification { return (new MailMessage) ->line('You are receiving this email because we received a password reset request for your account.') - ->action('Reset Password', url('password/reset/'.$this->token).'?email='.urlencode($notifiable->email)) + ->action('Reset Password', front_url('password/reset/'.$this->token).'?email='.urlencode($notifiable->email)) ->line('If you did not request a password reset, no further action is required.'); } } diff --git a/app/Notifications/Subscription/FailedPaymentNotification.php b/app/Notifications/Subscription/FailedPaymentNotification.php index f50a93f..dcb3970 100644 --- a/app/Notifications/Subscription/FailedPaymentNotification.php +++ b/app/Notifications/Subscription/FailedPaymentNotification.php @@ -36,6 +36,6 @@ class FailedPaymentNotification extends Notification implements ShouldQueue ->line(__('Please go to OpenForm, click on your name on the top right corner, and click on "Billing". You will then be able to update your card details. To avoid any service disruption, you can reply to this email whenever you updated your card details, and we\'ll manually attempt to charge your card.')) - ->action(__('Go to OpenForm'), url('/')); + ->action(__('Go to OpenForm'), front_url('/')); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index faee4fa..989bcff 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -19,15 +19,6 @@ class RouteServiceProvider extends ServiceProvider */ public const HOME = '/home'; - /** - * The controller namespace for the application. - * - * When present, controller route declarations will automatically be prefixed with this namespace. - * - * @var string|null - */ - // protected $namespace = 'App\\Http\\Controllers'; - /** * Define your route model bindings, pattern filters, etc. * @@ -39,19 +30,9 @@ class RouteServiceProvider extends ServiceProvider $this->registerGlobalRouteParamConstraints(); $this->routes(function () { - - Route::prefix('api') - ->middleware('api') + Route::middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); - - Route::middleware('web') - ->namespace($this->namespace) - ->group(base_path('routes/web.php')); - - Route::middleware('spa') - ->namespace($this->namespace) - ->group(base_path('routes/spa.php')); }); } diff --git a/app/Service/Forms/Webhooks/DiscordHandler.php b/app/Service/Forms/Webhooks/DiscordHandler.php index 559de29..f7529f5 100644 --- a/app/Service/Forms/Webhooks/DiscordHandler.php +++ b/app/Service/Forms/Webhooks/DiscordHandler.php @@ -27,7 +27,7 @@ class DiscordHandler extends AbstractWebhookHandler $externalLinks[] = '[**🔗 Open Form**](' . $this->form->share_url . ')'; } if(Arr::get($settings, 'link_edit_form', true)){ - $editFormURL = url('forms/' . $this->form->slug . '/show'); + $editFormURL = front_url('forms/' . $this->form->slug . '/show'); $externalLinks[] = '[**✍️ Edit Form**](' . $editFormURL . ')'; } if (Arr::get($settings, 'link_edit_submission', true) && $this->form->editable_submissions) { diff --git a/app/Service/Forms/Webhooks/SlackHandler.php b/app/Service/Forms/Webhooks/SlackHandler.php index 5b2faf6..f237efa 100644 --- a/app/Service/Forms/Webhooks/SlackHandler.php +++ b/app/Service/Forms/Webhooks/SlackHandler.php @@ -27,7 +27,7 @@ class SlackHandler extends AbstractWebhookHandler $externalLinks[] = '*<' . $this->form->share_url . '|🔗 Open Form>*'; } if(Arr::get($settings, 'link_edit_form', true)){ - $editFormURL = url('forms/' . $this->form->slug . '/show'); + $editFormURL = front_url('forms/' . $this->form->slug . '/show'); $externalLinks[] = '*<' . $editFormURL . '|✍️ Edit Form>*'; } if (Arr::get($settings, 'link_edit_submission', true) && $this->form->editable_submissions) { diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..e287b47 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,11 @@ + +
+ + + + + + + + + + + + +
+ + + diff --git a/client/components/forms/CheckboxInput.vue b/client/components/forms/CheckboxInput.vue new file mode 100644 index 0000000..fa37fd4 --- /dev/null +++ b/client/components/forms/CheckboxInput.vue @@ -0,0 +1,46 @@ + + + diff --git a/client/components/forms/CodeInput.client.vue b/client/components/forms/CodeInput.client.vue new file mode 100644 index 0000000..e276856 --- /dev/null +++ b/client/components/forms/CodeInput.client.vue @@ -0,0 +1,63 @@ + + + diff --git a/client/components/forms/ColorInput.vue b/client/components/forms/ColorInput.vue new file mode 100644 index 0000000..3bfa0e8 --- /dev/null +++ b/client/components/forms/ColorInput.vue @@ -0,0 +1,45 @@ + + + diff --git a/client/components/forms/DateInput.vue b/client/components/forms/DateInput.vue new file mode 100644 index 0000000..44afd99 --- /dev/null +++ b/client/components/forms/DateInput.vue @@ -0,0 +1,187 @@ + + + diff --git a/resources/js/components/forms/FileInput.vue b/client/components/forms/FileInput.vue similarity index 82% rename from resources/js/components/forms/FileInput.vue rename to client/components/forms/FileInput.vue index 0b041e6..4d904dc 100644 --- a/resources/js/components/forms/FileInput.vue +++ b/client/components/forms/FileInput.vue @@ -1,21 +1,10 @@ - - - - - + + + diff --git a/resources/js/components/forms/ScaleInput.vue b/client/components/forms/ScaleInput.vue similarity index 68% rename from resources/js/components/forms/ScaleInput.vue rename to client/components/forms/ScaleInput.vue index ea162cf..5eef878 100644 --- a/resources/js/components/forms/ScaleInput.vue +++ b/client/components/forms/ScaleInput.vue @@ -1,14 +1,10 @@ \ No newline at end of file + diff --git a/client/components/forms/SignatureInput.vue b/client/components/forms/SignatureInput.vue new file mode 100644 index 0000000..3859127 --- /dev/null +++ b/client/components/forms/SignatureInput.vue @@ -0,0 +1,62 @@ + + + diff --git a/client/components/forms/TextAreaInput.vue b/client/components/forms/TextAreaInput.vue new file mode 100644 index 0000000..6929602 --- /dev/null +++ b/client/components/forms/TextAreaInput.vue @@ -0,0 +1,56 @@ +