Skip to content

Commit

Permalink
Validate image url by fetching it, closes #18
Browse files Browse the repository at this point in the history
  • Loading branch information
felixbrucker committed Nov 17, 2023
1 parent a5ebe80 commit e50d4f9
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 37 deletions.
24 changes: 14 additions & 10 deletions src/app/update-account/update-account.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ <h6 class="settings-modal-text">{{snippetService.getSnippet('update-name-modal.t
<h6 class="pt-2 settings-modal-text">Account Image URL</h6>
<div class="row">
<div class="col-12">
<input type="text"
class="form-control"
aria-describedby="imageUrlFeedback"
placeholder="https://your-image-here.jpg"
[class.is-invalid]="!isNewImageUrlValid"
[(ngModel)]="newImageUrl"
>
<div id="imageUrlFeedback" class="invalid-feedback">
Not a valid url
</div>
<form [formGroup]="imageUrlForm">
<div class="d-flex gap-2" [class.is-invalid]="imageUrlForm.invalid">
<input type="text"
class="form-control"
formControlName="imageUrl"
aria-describedby="imageUrlFeedback"
placeholder="https://your-image-here.jpg"
>
<app-loading-state *ngIf="isValidatingImageUrl" width="2" height="2"></app-loading-state>
</div>
<div id="imageUrlFeedback" class="invalid-feedback">
Not a valid url
</div>
</form>
</div>
</div>
<div class="row mt-2">
Expand Down
85 changes: 58 additions & 27 deletions src/app/update-account/update-account.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
import {AccountService, UpdateAccountSettingsOptions} from '../account.service'
import {ToastService} from '../toast.service'
import {SnippetService} from '../snippet.service'
import {AsyncValidatorFn, FormBuilder} from '@angular/forms'
import {debounceTime, switchMap} from 'rxjs'
import {distinctUntilChanged, first, map} from 'rxjs/operators'
import {fromPromise} from 'rxjs/internal/observable/innerFrom'
import axios from 'axios'

@Component({
selector: 'app-update-account',
Expand All @@ -11,58 +16,51 @@ import {SnippetService} from '../snippet.service'
})
export class UpdateAccountComponent {
public newName = null
public newImageUrl?: string
public faCircleNotch = faCircleNotch
public isValidatingImageUrl: boolean = false
public readonly faCircleNotch = faCircleNotch
public readonly imageUrlForm = this.formBuilder.group({
imageUrl: [
this.accountService.account?.settings.profile?.imageUrl,
{
asyncValidators: [
this.makeImageUrlValidator(),
],
updateOn: 'change',
},
],
})

private get newImageUrlTrimmed(): string|undefined {
const newImageUrl = this.newImageUrl?.trim()
if (newImageUrl === '') {
return undefined
}
public get newImageUrl(): string|undefined {
const value = this.imageUrlForm.controls['imageUrl'].getRawValue()

return newImageUrl
return value?.trim() === '' ? undefined : value.trim()
}

constructor(
public accountService: AccountService,
public snippetService: SnippetService,
private readonly toastService: ToastService,
private readonly formBuilder: FormBuilder,
) {
this.newName = this.accountService.account.name
this.newImageUrl = this.accountService.account?.settings.profile?.imageUrl
}

get isNewNameValid(): boolean {
return !this.newNameOrUndefined || this.newNameOrUndefined.length <= 64
}

public get isNewImageUrlValid(): boolean {
if (this.newImageUrlTrimmed === undefined) {
return true
}

let url: URL
try {
url = new URL(this.newImageUrlTrimmed)
} catch (_) {
return false
}

return url.protocol === 'https:' && this.newImageUrlTrimmed.length < 1024
}

get canUpdateAccount(): boolean {
if (this.accountService.isUpdatingAccount) {
return false
}
if (!this.isNewNameValid || !this.isNewImageUrlValid) {
if (!this.isNewNameValid || !this.imageUrlForm.valid) {
return false
}
if (this.accountService.account.name !== this.newNameOrUndefined) {
return true
}

return this.accountService.account?.settings.profile?.imageUrl !== this.newImageUrlTrimmed
return this.accountService.account?.settings.profile?.imageUrl !== this.newImageUrl
}

get newNameOrUndefined(): string|undefined {
Expand All @@ -81,7 +79,7 @@ export class UpdateAccountComponent {
willUpdateName = true
}
let willUpdateImageUrl = false
const newImageUrl = this.newImageUrlTrimmed
const newImageUrl = this.newImageUrl
if (newImageUrl !== this.accountService.account?.settings.profile?.imageUrl) {
updateOptions.imageUrl = newImageUrl
willUpdateImageUrl = true
Expand All @@ -106,4 +104,37 @@ export class UpdateAccountComponent {
this.toastService.showErrorToast(err.message)
}
}

private makeImageUrlValidator(): AsyncValidatorFn {
return control => control.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(value => fromPromise(this.isValidImageUrl(value))),
map((isValid: boolean) => (isValid ? null : { invalid: true })),
first(),
)
}

private async isValidImageUrl(imageUrl: string|undefined): Promise<boolean> {
imageUrl = imageUrl?.trim() === '' ? undefined : imageUrl.trim()
if (imageUrl === undefined) {
return true
}

this.isValidatingImageUrl = true
try {
const url = new URL(imageUrl)

if (url.protocol !== 'https:' || imageUrl.length >= 1024) {
return false
}
await axios.get(imageUrl)

return true
} catch (_) {
return false
} finally {
this.isValidatingImageUrl = false
}
}
}

0 comments on commit e50d4f9

Please sign in to comment.