edalzell / statamic-profiler

Update user data from the front end
4 stars 1 forks source link

Asset upload isn't validating #17

Open danielfowler opened 5 years ago

danielfowler commented 5 years ago

I have a validate line in my user.yaml to restrict file size to 1MB and file type to JPG, but I am getting all kinds of files uploaded (even PDF and DOCX) of massive sizes (some 8MB).

      portrait:
        type: assets
        display: Portrait
        container: performer-portraits
        max_files: 1
        validate: max:1000|ext:jpg,jpeg
        width: 50

This might be a Workshop bug, since I know that Profiler is built upon Workshop, but I think it's worth noting.

edalzell commented 5 years ago

is that validate line right? no quotes around it? I would expect all those : on one line would cause a problem.

Profiler is not built on Workshop, it is completely separate.

danielfowler commented 5 years ago

I added quotation marks, I'll keep you updated as I see new assets come in.

aerni commented 4 years ago

Asset validation still doesn't work.

It works on initial user registration using {{ user:register_form files="true" }}. But it fails when trying to update the picture with {{ profiler:edit_form files="true" }}.

In my users.yaml.

profile_picture:
  container: users
  restrict: true
  max_files: 1
  type: assets
  display: Profilbild
  folder: /
  validate: required|mimes:jpg,jpeg,png|max:3000

I tried validate: "required|mimes:jpg,jpeg,png|max:3000" (with quotes) as well. This didn't work either.

I'm on Statamic v2.11.19 and Profiler v2.1.4.

aerni commented 4 years ago

Actually it looks like the fix from https://github.com/edalzell/statamic-profiler/commit/d15d94026efabc603c9800de89eaf5b4005ecb1c is not present in v2.1.4.

aerni commented 4 years ago

It looks like the fix was still present in Tag: 2.1.2. It's missing from Tag: 2.1.3 upwards.

I've noticed that use Statamic\CP\Publish\ValidationBuilder in ProfilerController.php is missing from Tag: 2.1.3 upwards as well. Not sure if that's another hiccup or meant to be. I'd be carefully checking everything.

aerni commented 4 years ago

I've encountered another bug #24 which is possibly related to this as well.

edalzell commented 4 years ago

This only occurs when you set it required but don't include a file

edalzell commented 4 years ago

@aerni question for you, IF they already have a photo attached (because it works during user registration) and they only want to change, say, their last name, and NOT their photo, then how do you want it to work?

Right now I assume that if there is no photo in the request, then they do not want to update it so I remove the validation rules. This seems reasonable to me, does this not work for you?

If not, can you please how you'd like it to work?

aerni commented 4 years ago

Yes, that's exactly how I'd expect it to work. I think this goes along with what I was trying to explain in issue #24.

Let me explain a bit more. All the fields from the user.yaml are present in the user registration from. The validation should work accordingly. All the required fields have to be filled.

Now when the user edits his profile, I ONLY want to validate the fields present in the user edit form. I DON'T want to get an error of ANY required field that is NOT present in the form.

I have a form to edit the user account. This form only includes fields to edit username and password. I can successfully send the form and don't have to include all the other required fields from the user.yaml, as those fields were already published during registration. This is the expected behavior.

Now I have another form to edit the user profile with his profile_picture, name, email etc. This is where the validation for the privacy_terms checkbox fails. But it ONLY fails when the profile_picture validation fails. It DOESN'T fail when any other field is successfully changed or the validation of any other field fails.

I hope this makes sense. I can also give you access to the project repo if that helps you track it down.

This is my user.yaml:

sections:
  main:
    display: Main
    fields:
      profile_picture:
        container: users
        restrict: true
        max_files: 1
        type: assets
        display: Profilbild
        folder: /
        validate: required|image|mimes:jpg,jpeg,png|max:3000
      username:
        type: text
        display: Benutzername
        validate: required|string|alpha_dash|max:30
        width: 33
      name:
        type: text
        display: Name
        validate: 'required|string|max:255|regex:/^[\p{Latin}\s\-]+$/u'
        width: 33
      email:
        type: text
        display: E-Mail
        validate: required|string|email|max:255
        width: 33
      zip:
        type: text
        display: PLZ
        validate: required|string|integer|digits:4
        width: 33
      city:
        type: text
        display: Ort
        validate: 'required|string|max:255|regex:/^[\p{Latin}\s\-\d]+$/u'
        width: 33
      cantons:
        max_items: 1
        type: taxonomy
        display: Kanton
        taxonomy: cantons
        mode: tags
        create: false
        validate: required|string|alpha_dash
        width: 33
      services:
        type: taxonomy
        display: Services
        taxonomy: services
        mode: tags
        create: false
        validate: required|array
        width: 50
      radius:
        max_items: 1
        type: taxonomy
        display: Umkreis
        taxonomy: radius
        mode: tags
        create: false
        validate: required|string|alpha_dash
        width: 50
      privacy_terms:
        type: checkboxes
        options:
          accepted: Akzeptiert
        display: Datenschutz und Bedingungen
        validate: 'required|string|regex:/^accepted$/'
      roles:
        type: user_roles
taxonomies: false
hide: true
title: User
edalzell commented 4 years ago

Now when the user edits his profile, I ONLY want to validate the fields present in the user edit form. I DON'T want to get an error of ANY required field that is NOT present in the form.

Ya that's not going to happen, use hidden fields for that. Too hard to figure out when someone wants to REMOVE some data vs keep if the data isn't there in both cases.

edalzell commented 4 years ago

also for your file checking it should be ext:jpg... not mime, that doesn't seem to work in Statamic.

aerni commented 4 years ago

True. I didn‘t consider the use case of a user wanting to delete some data.

I experimented with the file check using both ext and mime. What I found is that the assets fieldtype in the CP can only validate with ext, and the registration form on the frontend only with mime.

The CP doesn‘t work with mime because Statamic doesn‘t actually validate the picture but the array. There‘s a thread on the Statamic forum about this.

I didn‘t find why the file input on the fronend doesn‘t work with ext though.

edalzell commented 4 years ago

it works for me w/ ext in my testing. Regardless, that's not a Profiler issue.

aerni commented 4 years ago

What works with ext for you? Registration or Profiler or both?

For me, ext doesn't work on registration, which is why I switched to this validation which works great using the registration form: validate: required|image|mimes:jpg,jpeg,png|max:3000. It only fails in the CP, because the assets fieldtype doesn't like mimes. But in my case, I just ignore this, because validating the registration is more important than CP input.

When editing the user with Profiler, the validation of mimes fails. Profiler works fine with ext. So it's exactly the opposite of the registration form. Also, Profiler doesn't like the max validation.

In short, this validation works fine on registration: validate: required|image|mimes:jpg,jpeg,png|max:3000

This validation works with Profiler: validate: required|ext:jpg,jpeg,png

Let me know if you'd like access to my repo for a real test case.

edalzell commented 4 years ago

I call the built in validators and have nothing to with the actual validation. No clue why one works on registration vs profiler.

I wonder if the form tags do something special?

I'll leave this open and take a look.

aerni commented 4 years ago

Did you have a chance to look at this yet?

It's a really odd issue. Why would Profiler's asset validation fail different to the registration form …

Especially weird is the behaviour I explained earlier:

I have some checkboxes and radio buttons on the frontend which should validate according to the settings in user.yaml. The fields are services, radius and privacy_terms. They validate as expected in the registration form. But NOT in Profiler's edit form.

Profiler DOESN'T throw any validation errors for those fields. BUT it throws validation errors for those fields WHEN the assets validation fails. And ONLY when the assets validation fails.

All the other validations work as expected.

That's why I suspect something funky with Profiler's asset validation. But maybe that's a Statamic core issue? No idea …

This is my user.yaml:

sections:
  main:
    display: Main
    fields:
      profile_picture:
        container: users
        restrict: true
        max_files: 1
        type: assets
        display: Profilbild
        folder: /
        validate: required|image|mimes:jpg,jpeg,png|max:3000
      username:
        type: text
        display: Benutzername
        validate: required|string|alpha_dash|max:30
        width: 33
      name:
        type: text
        display: Name
        validate: 'required|string|max:255|regex:/^[\p{Latin}\s\-]+$/u'
        width: 33
      email:
        type: text
        display: E-Mail
        validate: required|string|email|max:255
        width: 33
      zip:
        type: text
        display: PLZ
        validate: required|string|integer|digits:4
        width: 33
      city:
        type: text
        display: Ort
        validate: 'required|string|max:255|regex:/^[\p{Latin}\s\-\d]+$/u'
        width: 33
      cantons:
        max_items: 1
        type: taxonomy
        display: Kanton
        taxonomy: cantons
        mode: tags
        create: false
        validate: required|string|alpha_dash
        width: 33
      services:
        type: taxonomy
        display: Services
        taxonomy: services
        mode: tags
        create: false
        validate: required|array
        width: 50
      radius:
        max_items: 1
        type: taxonomy
        display: Umkreis
        taxonomy: radius
        mode: tags
        create: false
        validate: required|string|alpha_dash
        width: 50
      privacy_terms:
        type: checkboxes
        options:
          accepted: Akzeptiert
        display: Datenschutz und Bedingungen
        validate: 'required|string|regex:/^accepted$/'
      roles:
        type: user_roles
taxonomies: false
hide: true
title: User
edalzell commented 4 years ago

Can I see your form as well @aerni? I wouldn't expect that privacy_terms validation to work as it's an array of data and you're checking it for string etc. Same w/ radius actually, it's an array and you're checking for string

edalzell commented 4 years ago

And your user registration template please @aerni.

aerni commented 4 years ago

Can I see your form as well @aerni? I wouldn't expect that privacy_terms validation to work as it's an array of data and you're checking it for string etc. Same w/ radius actually, it's an array and you're checking for string

Yeah I know, it's odd but it works. At least on registration. Because it's saved as a string when there's only one option available.

Please find all the data below. I'm happy to give you access to the project, as it may be easier to track it down. Or do a remote pair to show you the error. It's easier to grasp when you see it in action than reading through this. At least for me. 😅

Observations and Possible Issue

In my countless tests with the profile_picture validation, I found that the following validation works with the {{ user:register_form }}. BUT NOT with the {{ profiler:edit_form }} nor in the CP:

validate: required|image|mimes:jpg,jpeg,png|max:3000

This validation works with the {{ profiler:edit_form }} and in the CP. BUT NOT with the {{ user:register_form }}:

validate: required|ext:jpg,jpeg,png

It looks like the {{ profiler:edit_form }} and the CP behave the same way and expect the same validation.

Could it be that the {{ user:register_form }} uses a different validator than the CP uses? And that {{ profiler:edit_form }} is making use of the CP validator and thus not validating the same as the {{ user:register_form }}?

Statamic Registration Form

This form works perfectly as expected, respecting all the validation rules set in the user.yaml.

{{ user:register_form redirect="/success?register=true" files="true" attr="x-data:{ submitting: false }|@submit:submitting = true" }}

    <div>
        {{ if errors }}
            <div class="p-4 mb-6 rounded-md bg-red-100">
                <p class="rfs-text-sm font-medium text-red-600">
                    {{ errors }}
                        {{ value }}<br>
                    {{ /errors }}
                </p>
            </div>
        {{ /if }}
        <div>
            <h3 class="rfs-text-lg leading-6 font-medium text-gray-900">Account</h3>
            <p class="mt-1 rfs-text-sm leading-5 text-gray-500">Lege Benutzername und Passwort fest.</p>
        </div>
        <div class="mt-6 grid grid-cols-1 row-gap-6 col-gap-4 sm:grid-cols-6">
            <div class="sm:col-span-6">
                <label for="username" class="block rfs-text-sm font-medium leading-5 text-gray-700">Benutzername</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="username" type="text" name="username" value="{{ old:username }}"  class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-3">
                <label for="password" class="block rfs-text-sm font-medium leading-5 text-gray-700">Passwort</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="password" type="password" name="password" value="{{ old:password }}"  class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-3">
                <label for="password_confirmation" class="block rfs-text-sm font-medium leading-5 text-gray-700">Passwort wiederholen</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="password_confirmation" type="password" name="password_confirmation" value="{{ old:password_confirmation }}"  class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>
        </div>
    </div>

    <div class="mt-8 border-t border-gray-200 pt-8">
        <div>
            <h3 class="rfs-text-lg leading-6 font-medium text-gray-900">Persönliche Informationen</h3>
            <p class="mt-1 rfs-text-sm leading-5 text-gray-500">Diese Informationen werden öffentlich in deinem Profil angezeigt.</p>
        </div>
        <div class="mt-6 grid grid-cols-1 row-gap-6 col-gap-4 sm:grid-cols-6">

            <div x-data="profilePicture()" class="sm:col-span-6">
                <label for="profile_picture" class="rfs-text-sm leading-5 font-medium text-gray-700 select-none">Profilbild</label>
                <div class="mt-2 flex items-center">
                    <div @click="$refs.input.click()" class="h-12 w-12 flex-shrink-0 rounded-full overflow-hidden bg-gray-200 cursor-pointer">
                        <div class="aspect-ratio-square relative">
                            <div class="absolute inset-0 w-full h-full flex justify-center items-center">
                                {{ theme:output src="svg/user-solid.svg" }}
                            </div>
                            {{ if profile_picture }}
                                {{ asset:profile_picture }}
                                    {{ if is_image }}
                                        <img
                                            x-ref="preview"
                                            src="{{ glide:url preset="user_thumbnail" }}"
                                            alt="{{ name }}"
                                            class="absolute inset-0 w-full h-full object-cover"
                                        />
                                    {{ /if }}
                                {{ /asset:profile_picture }}
                            {{ else }}
                                <img
                                    x-ref="preview"
                                    src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
                                    class="absolute inset-0 w-full h-full object-cover"
                                />
                            {{ /if }}
                        </div>
                    </div>
                    <span class="ml-5 rounded-md shadow-sm">
                        <button @click="$refs.input.click()" type="button" class="py-2 px-3 border border-gray-300 rounded-md text-sm leading-4 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition duration-150 ease-in-out">
                            Bild ändern
                            <span class="rfs-text-sm leading-5 text-gray-500">/ JPG, PNG bis 3MB</span>
                        </button>
                    </span>
                    <input x-ref="input" @change="updatePreview()" type="file" id="profile_picture" name="profile_picture" accept=".jpg, .jpeg, .png" class="w-0 opacity-0" />
                </div>
            </div>

            <script>
                function profilePicture() {
                  return {
                    fileTypes: [
                      'image/jpg',
                      'image/jpeg',
                      'image/png',
                    ],
                    validFileType(file) {
                      return this.fileTypes.includes(file.type);
                    },
                    updatePreview() {
                      if (this.$refs.input.files.length === 0) return;
                      if (this.validFileType(this.$refs.input.files[0])) {
                        this.$refs.preview.src = URL.createObjectURL(this.$refs.input.files[0]);
                      }
                    },
                  };
                }
            </script>

            <div class="sm:col-span-3">
                <label for="name" class="block rfs-text-sm font-medium leading-5 text-gray-700">Name</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="name" type="text" name="name" value="{{ old:name }}"  class="form-input block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-3">
                <label for="email" class="block rfs-text-sm font-medium leading-5 text-gray-700">E-Mail</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="email" type="email" name="email" value="{{ old:email }}"  class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-2">
                <label for="zip" class="block rfs-text-sm font-medium leading-5 text-gray-700">PLZ</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="zip" type="text" name="zip" value="{{ old:zip }}"  class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-2">
                <label for="city" class="block rfs-text-sm font-medium leading-5 text-gray-700">Ort</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="city" type="text" name="city" value="{{ old:city }}"  class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-2">
                <label for="canton" class="block rfs-text-sm font-medium leading-5 text-gray-700">Kanton</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <select id="canton" name="cantons"  class="form-select block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5">
                        <option selected disabled>Kanton wählen …</option>
                        {{ relate:general:cantons_listing_order }}
                            <option value="{{ slug }}" {{ if old:cantons === slug }}selected{{ /if }}>{{ title }}</option>
                        {{ /relate:general:cantons_listing_order }}
                    </select>
                </div>
            </div>
        </div>
    </div>

    <div class="mt-8 border-t border-gray-200 pt-8">
        <div>
            <h3 class="rfs-text-lg leading-6 font-medium text-gray-900">Services & Umkreis</h3>
            <p class="mt-1 rfs-text-sm leading-5 text-gray-500">Wähle deine Services und den Umkreis, in dem du diese anbieten möchtest.</p>
        </div>
        <div class="mt-6">
            <fieldset>
                <legend class="rfs-text-base font-medium text-gray-900">Services</legend>
                <div class="flex flex-wrap">
                    {{ relate:general:services_listing_order }}
                        <div class="mt-4 mr-6">
                            <div class="relative flex items-start">
                                <div class="absolute flex items-center h-5">
                                    <input id="{{ slug }}" type="checkbox" name="services[]" value="{{ slug }}" {{ if (old:services | in_array:slug) }}checked{{ /if }} class="form-checkbox h-4 w-4 text-yellow-300 transition duration-150 ease-in-out" />
                                </div>
                                <div class="pl-7 rfs-text-sm leading-5">
                                    <label for="{{ slug }}" class="font-medium text-gray-700">{{ title }}</label>
                                </div>
                            </div>
                        </div>
                    {{ /relate:general:services_listing_order }}
                </div>
            </fieldset>

            <fieldset class="mt-6">
                <legend class="rfs-text-base font-medium text-gray-900">Umkreis</legend>
                <div class="flex flex-wrap">
                    {{ relate:general:radius_listing_order }}
                        <div class="mt-4 mr-6 flex items-center">
                            <input id="{{ slug }}" type="radio" name="radius" value="{{ slug }}" {{ if old:radius === slug }}checked{{ /if }}  class="form-radio h-4 w-4 text-yellow-300 transition duration-150 ease-in-out" />
                            <label for="{{ slug }}" class="ml-3">
                                <span class="block rfs-text-sm leading-5 font-medium text-gray-700">{{ title }}</span>
                            </label>
                        </div>
                    {{ /relate:general:radius_listing_order }}
                </div>
            </fieldset>
        </div> 
    </div>

    <div class="mt-8 border-t border-gray-200 pt-8">
        <div>
            <h3 class="rfs-text-lg leading-6 font-medium text-gray-900">Datenschutzerklärung</h3>
        </div>
        <div class="mt-6">
            <div class="relative flex items-start">
                <div class="absolute flex items-center h-5">
                    <input id="privacy_terms" type="checkbox" name="privacy_terms" value="accepted" {{ if old:privacy_terms }}checked{{ /if }}  class="form-checkbox h-4 w-4 text-yellow-300 transition duration-150 ease-in-out" />
                </div>
                <div class="pl-7 rfs-text-sm leading-5">
                    <label for="privacy_terms" class="font-medium text-gray-700">Ich akzeptiere die <a href="https://www.iubenda.com/privacy-policy/82360217" target="_blank" class="text-yellow-300">Datenschutzerklärung</a> und bestätige, dass ich 18+ Jahre alt bin.</label>
                </div>
            </div>
        </div> 
    </div>

    <div class="mt-8 border-t border-gray-200 pt-5">
        <div class="flex justify-end">
            <span class="inline-flex rounded-md shadow-sm">
                <button :disabled="submitting" :class="{ 'bg-yellow-400 cursor-not-allowed': submitting }" type="submit" class="inline-flex justify-center items-center py-2 px-4 border border-transparent rfs-text-sm leading-5 font-medium rounded-md text-white bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:border-yellow-400 focus:shadow-outline-yellow active:bg-yellow-400 transition duration-150 ease-in-out">
                    <i x-show="submitting" x-cloak class="fad fa-spinner-third fa-spin rfs-text-lg mr-2"></i>
                    Registrieren
                </button>
            </span>
        </div>
    </div>

{{ /user:register_form }}

User Data From Registration

This is the data that's saved when a user registeres through the registration form.

name: Michael
email: hello@michaelaerni.ch
zip: '3174'
city: Thörishaus
cantons: bern
services:
  - einkaufen
  - gespraech
radius: 5-km
privacy_terms: accepted
profile_picture: /assets/users/michael_ciy.jpg
roles:
  - 1c1159d7-0ea7-4f3d-bbf4-42e611c81552
password_hash: $2y$10$.0WT0oHdvELjBFLGwqt2y.wdscDxyzG8NJ.yirSGtqiq3/XRc0kli
id: af586d8f-0dfc-40de-a53e-68468203aeb8

Profiler Edit Form

This form is different to the registration form, as it doesn't contain the username and password. It still validates though, because username and password are already set in the user's yaml file. In the same matter, I could remove the privacy_terms checkbox or any other required fields that already have a value set in the user's yaml.

Saving the user works perfectly without having this checkbox or any other required elements present in the form. BUT as soon as the profile_picture validation fails, it ALSO throws a validation error for privacy_terms or any other expected form value.

This can be recreated by deleting some required data like radius from the user's yaml and editing the user through this form. The validation WILL pass and not throw an error. Which it shouldn't. However, it WILL throw the error when the profile_picture validation fails. BUT ONLY then. To test this, you have to remove the required attributes on the HTML form elements.

Likewise you can delete a required element from the form altogether. Like it did with username and password. Try deleting the radius form element in the HTML. Validation will still pass, as long as the data is already set in the user's yaml. But it WILL fail, as soon as the profile_picture validation fails.

It will ALSO fail when the profile_picture validation passes. Remove the validation from the profile_picture and update the picture through the form. Now all the required data that's missing a HTML form element is throwing an error. It seems like the validation is only triggered when the profile_picture changes. It is NOT triggered, however, when any other user data is changed. ONLY when the profile_picture either changes or the validation fails. Try to just update another form value without changing the profile_picture. No error will be thrown.

{{ profiler:edit_form redirect="/profile" files="true" attr="x-data:{ submitting: false }|@submit:submitting = true" }}

    <div>
        {{ if errors }}
            <div class="p-4 mb-6 rounded-md bg-red-100">
                <p class="rfs-text-sm font-medium text-red-600">
                    {{ errors }}
                        {{ value }}<br>
                    {{ /errors }}
                </p>
            </div>
        {{ /if }}
        {{ if success }}
            <div class="p-4 mb-6 rounded-md bg-green-100">
                <p class="rfs-text-sm font-medium text-green-600">
                    Dein Profil wurde erfolgreich aktualisiert.
                </p>
            </div>
        {{ /if }}
        <div>
            <h3 class="rfs-text-lg leading-6 font-medium text-gray-900">Persönliche Informationen</h3>
            <p class="mt-1 rfs-text-sm leading-5 text-gray-500">Diese Informationen werden öffentlich in deinem Profil angezeigt.</p>
        </div>
        <div class="mt-6 grid grid-cols-1 row-gap-6 col-gap-4 sm:grid-cols-6">

            <div x-data="profilePicture()" class="sm:col-span-6">
                <label for="profile_picture" class="rfs-text-sm leading-5 font-medium text-gray-700 select-none">Profilbild</label>
                <div class="mt-2 flex items-center">
                    <div @click="$refs.input.click()" class="h-12 w-12 flex-shrink-0 rounded-full overflow-hidden bg-gray-200 cursor-pointer">
                        <div class="aspect-ratio-square relative">
                            <div class="absolute inset-0 w-full h-full flex justify-center items-center">
                                {{ theme:output src="svg/user-solid.svg" }}
                            </div>
                            {{ if profile_picture }}
                                {{ asset:profile_picture }}
                                    {{ if is_image }}
                                        <img
                                            x-ref="preview"
                                            src="{{ glide:url preset="user_thumbnail" }}"
                                            alt="{{ name }}"
                                            class="absolute inset-0 w-full h-full object-cover"
                                        />
                                    {{ /if }}
                                {{ /asset:profile_picture }}
                            {{ else }}
                                <img
                                    x-ref="preview"
                                    src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
                                    class="absolute inset-0 w-full h-full object-cover"
                                />
                            {{ /if }}
                        </div>
                    </div>
                    <span class="ml-5 rounded-md shadow-sm">
                        <button @click="$refs.input.click()" type="button" class="py-2 px-3 border border-gray-300 rounded-md text-sm leading-4 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition duration-150 ease-in-out">
                            Bild ändern
                            <span class="rfs-text-sm leading-5 text-gray-500">/ JPG, PNG bis 3MB</span>
                        </button>
                    </span>
                    <input x-ref="input" @change="updatePreview()" type="file" id="profile_picture" name="profile_picture" accept=".jpg, .jpeg, .png" class="w-0 opacity-0" />
                </div>
            </div>

            <script>
                function profilePicture() {
                  return {
                    fileTypes: ['image/jpg', 'image/jpeg', 'image/png'],
                    validFileType(file) {
                      return this.fileTypes.includes(file.type);
                    },
                    updatePreview() {
                      if (this.$refs.input.files.length === 0) return;
                      if (this.validFileType(this.$refs.input.files[0])) {
                        this.$refs.preview.src = URL.createObjectURL(this.$refs.input.files[0]);
                      }
                    },
                  };
                }
            </script>

            <div class="sm:col-span-3">
                <label for="name" class="block rfs-text-sm font-medium leading-5 text-gray-700">Name</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="name" type="text" name="name" value="{{ old:name or name }}" placeholder="{{ name }}" required class="form-input block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-3">
                <label for="email" class="block rfs-text-sm font-medium leading-5 text-gray-700">E-Mail</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="email" type="email" name="email" value="{{ old:email or email }}" placeholder="{{ email }}" required class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-2">
                <label for="zip" class="block rfs-text-sm font-medium leading-5 text-gray-700">PLZ</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="zip" type="text" name="zip" value="{{ old:zip or zip }}" placeholder="{{ zip }}" required class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-2">
                <label for="city" class="block rfs-text-sm font-medium leading-5 text-gray-700">Ort</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <input id="city" type="text" name="city" value="{{ old:city or city }}" placeholder="{{ city }}" required class="form-input block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5" />
                </div>
            </div>

            <div class="sm:col-span-2">
                <label for="canton" class="block rfs-text-sm font-medium leading-5 text-gray-700">Kanton</label>
                <div class="mt-1 rounded-md shadow-sm">
                    <select id="canton" name="cantons" required class="form-select block w-full transition duration-150 ease-in-out sm:rfs-text-sm sm:leading-5">
                        {{ relate:general:cantons_listing_order }}
                            <option {{ if cantons === slug }}selected{{ /if }} value="{{ slug }}">{{ title }}</option>
                        {{ /relate:general:cantons_listing_order }}
                    </select>
                </div>
            </div>
        </div>
    </div>

    <div class="mt-8 border-t border-gray-200 pt-8">
        <div>
            <h3 class="rfs-text-lg leading-6 font-medium text-gray-900">Services & Umkreis</h3>
            <p class="mt-1 rfs-text-sm leading-5 text-gray-500">Wähle deine Services und den Umkreis, in dem du diese anbieten möchtest.</p>
        </div>
        <div class="mt-6">
            <fieldset>
                <legend class="rfs-text-base font-medium text-gray-900">Services</legend>
                <div class="flex flex-wrap">
                    {{ relate:general:services_listing_order }}
                        <div class="mt-4 mr-6">
                            <div class="relative flex items-start">
                                <div class="absolute flex items-center h-5">
                                    <input id="{{ slug }}" type="checkbox" name="services[]" value="{{ slug }}" {{ if (services | in_array:slug) }}checked{{ /if }} class="form-checkbox h-4 w-4 text-yellow-300 transition duration-150 ease-in-out" />
                                </div>
                                <div class="pl-7 rfs-text-sm leading-5">
                                    <label for="{{ slug }}" class="font-medium text-gray-700">{{ title }}</label>
                                </div>
                            </div>
                        </div>
                    {{ /relate:general:services_listing_order }}
                </div>
            </fieldset>

            <fieldset class="mt-6">
                <legend class="rfs-text-base font-medium text-gray-900">Umkreis</legend>
                <div class="flex flex-wrap">
                    {{ relate:general:radius_listing_order }}
                        <div class="mt-4 mr-6 flex items-center">
                            <input id="{{ slug }}" type="radio" name="radius" value="{{ slug }}" {{ if radius === slug }}checked{{ /if }} required class="form-radio h-4 w-4 text-yellow-300 transition duration-150 ease-in-out" />
                            <label for="{{ slug }}" class="ml-3">
                                <span class="block rfs-text-sm leading-5 font-medium text-gray-700">{{ title }}</span>
                            </label>
                        </div>
                    {{ /relate:general:radius_listing_order }}
                </div>
            </fieldset>
        </div> 
    </div>

    {{# This field is only in here, because validation fails when it's not. Weird thing is, that validation ONLY fails, if you change the profile picture. #}}
    <input id="privacy_terms" type="checkbox" name="privacy_terms" value="accepted" required checked class="hidden" />

    <div class="mt-8 border-t border-gray-200 pt-5">
        <div class="flex justify-end">
            <span class="inline-flex rounded-md shadow-sm">
                <button :disabled="submitting" :class="{ 'bg-yellow-400 cursor-not-allowed': submitting }" type="submit" class="inline-flex justify-center items-center py-2 px-4 border border-transparent rfs-text-sm leading-5 font-medium rounded-md text-white bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:border-yellow-400 focus:shadow-outline-yellow active:bg-yellow-400 transition duration-150 ease-in-out">
                    <i x-show="submitting" x-cloak class="fad fa-spinner-third fa-spin rfs-text-lg mr-2"></i>
                    Speichern
                </button>
            </span>
        </div>
    </div>

{{ /profiler:edit_form }}

I hope this all makes sense and that it helps you track it down. Let me know if you need anything more.