Improved upload flow, fixed vulnerability in search.
This commit is contained in:
parent
abde8bb815
commit
eb02563714
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewlalis.gymboard_api.config;
|
package nl.andrewlalis.gymboard_api.config;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dto.ApiValidationException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
@ -33,6 +34,9 @@ public class ErrorResponseHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responseContent.put("message", message);
|
responseContent.put("message", message);
|
||||||
|
if (e instanceof ApiValidationException validationException) {
|
||||||
|
responseContent.put("validation_messages", validationException.getValidationResponse().getMessages());
|
||||||
|
}
|
||||||
return ResponseEntity.status(e.getStatusCode()).body(responseContent);
|
return ResponseEntity.status(e.getStatusCode()).body(responseContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,6 @@ public class SecurityConfig {
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(// Allow the following POST endpoints to be public.
|
.requestMatchers(// Allow the following POST endpoints to be public.
|
||||||
HttpMethod.POST,
|
HttpMethod.POST,
|
||||||
"/gyms/*/submissions",
|
|
||||||
"/gyms/*/submissions/upload",
|
|
||||||
"/auth/token",
|
"/auth/token",
|
||||||
"/auth/register",
|
"/auth/register",
|
||||||
"/auth/activate",
|
"/auth/activate",
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package nl.andrewlalis.gymboard_api.config;
|
package nl.andrewlalis.gymboard_api.config;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
||||||
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
@ -11,4 +14,17 @@ public class WebComponents {
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder(10);
|
return new BCryptPasswordEncoder(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ULID ulid() {
|
||||||
|
return new ULID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value("${app.cdn-origin}")
|
||||||
|
private String cdnOrigin;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CdnClient cdnClient() {
|
||||||
|
return new CdnClient(cdnOrigin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
package nl.andrewlalis.gymboard_api.config;
|
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class WebConfig {
|
|
||||||
@Bean
|
|
||||||
public ULID ulid() {
|
|
||||||
return new ULID();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
public class ApiValidationException extends ResponseStatusException {
|
||||||
|
private final ValidationResponse validationResponse;
|
||||||
|
|
||||||
|
public ApiValidationException(ValidationResponse validationResponse) {
|
||||||
|
super(HttpStatus.BAD_REQUEST, "Validation failed.");
|
||||||
|
this.validationResponse = validationResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationResponse getValidationResponse() {
|
||||||
|
return validationResponse;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ValidationResponse {
|
||||||
|
private boolean valid = true;
|
||||||
|
private List<String> messages = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addMessage(String message) {
|
||||||
|
this.messages.add(message);
|
||||||
|
this.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,13 +3,13 @@ package nl.andrewlalis.gymboard_api.domains.api.service.submission;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.ExerciseRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.ExerciseRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
import nl.andrewlalis.gymboard_api.domains.api.dto.*;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionPayload;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
|
import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
|
import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.UploadsClient;
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
|
@ -36,16 +36,18 @@ public class ExerciseSubmissionService {
|
||||||
private final ExerciseRepository exerciseRepository;
|
private final ExerciseRepository exerciseRepository;
|
||||||
private final SubmissionRepository submissionRepository;
|
private final SubmissionRepository submissionRepository;
|
||||||
private final ULID ulid;
|
private final ULID ulid;
|
||||||
|
private final CdnClient cdnClient;
|
||||||
|
|
||||||
public ExerciseSubmissionService(GymRepository gymRepository,
|
public ExerciseSubmissionService(GymRepository gymRepository,
|
||||||
UserRepository userRepository, ExerciseRepository exerciseRepository,
|
UserRepository userRepository, ExerciseRepository exerciseRepository,
|
||||||
SubmissionRepository submissionRepository,
|
SubmissionRepository submissionRepository,
|
||||||
ULID ulid) {
|
ULID ulid, CdnClient cdnClient) {
|
||||||
this.gymRepository = gymRepository;
|
this.gymRepository = gymRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.exerciseRepository = exerciseRepository;
|
this.exerciseRepository = exerciseRepository;
|
||||||
this.submissionRepository = submissionRepository;
|
this.submissionRepository = submissionRepository;
|
||||||
this.ulid = ulid;
|
this.ulid = ulid;
|
||||||
|
this.cdnClient = cdnClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
@ -64,16 +66,22 @@ public class ExerciseSubmissionService {
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public SubmissionResponse createSubmission(CompoundGymId id, String userId, SubmissionPayload payload) {
|
public SubmissionResponse createSubmission(CompoundGymId id, String userId, SubmissionPayload payload) {
|
||||||
User user = userRepository.findById(userId)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
|
|
||||||
Gym gym = gymRepository.findByCompoundId(id)
|
Gym gym = gymRepository.findByCompoundId(id)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
|
||||||
|
if (!user.isActivated()) throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||||
Exercise exercise = exerciseRepository.findById(payload.exerciseShortName())
|
Exercise exercise = exerciseRepository.findById(payload.exerciseShortName())
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid exercise."));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid exercise."));
|
||||||
|
|
||||||
// TODO: Validate the submission data.
|
var validationResponse = validateSubmissionData(gym, user, exercise, payload);
|
||||||
|
if (!validationResponse.isValid()) {
|
||||||
|
throw new ApiValidationException(validationResponse);
|
||||||
|
}
|
||||||
|
|
||||||
// Create the submission.
|
// Create the submission.
|
||||||
|
LocalDateTime performedAt = payload.performedAt();
|
||||||
|
if (performedAt == null) performedAt = LocalDateTime.now();
|
||||||
BigDecimal rawWeight = BigDecimal.valueOf(payload.weight());
|
BigDecimal rawWeight = BigDecimal.valueOf(payload.weight());
|
||||||
WeightUnit weightUnit = WeightUnit.parse(payload.weightUnit());
|
WeightUnit weightUnit = WeightUnit.parse(payload.weightUnit());
|
||||||
BigDecimal metricWeight = BigDecimal.valueOf(payload.weight());
|
BigDecimal metricWeight = BigDecimal.valueOf(payload.weight());
|
||||||
|
@ -81,17 +89,47 @@ public class ExerciseSubmissionService {
|
||||||
metricWeight = WeightUnit.toKilograms(rawWeight);
|
metricWeight = WeightUnit.toKilograms(rawWeight);
|
||||||
}
|
}
|
||||||
Submission submission = submissionRepository.saveAndFlush(new Submission(
|
Submission submission = submissionRepository.saveAndFlush(new Submission(
|
||||||
ulid.nextULID(),
|
ulid.nextULID(), gym, exercise, user,
|
||||||
gym,
|
performedAt,
|
||||||
exercise,
|
|
||||||
user,
|
|
||||||
LocalDateTime.now(),
|
|
||||||
payload.videoFileId(),
|
payload.videoFileId(),
|
||||||
rawWeight,
|
rawWeight, weightUnit, metricWeight, payload.reps()
|
||||||
weightUnit,
|
|
||||||
metricWeight,
|
|
||||||
payload.reps()
|
|
||||||
));
|
));
|
||||||
return new SubmissionResponse(submission);
|
return new SubmissionResponse(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ValidationResponse validateSubmissionData(Gym gym, User user, Exercise exercise, SubmissionPayload data) {
|
||||||
|
ValidationResponse response = new ValidationResponse();
|
||||||
|
LocalDateTime cutoff = LocalDateTime.now().minusDays(3);
|
||||||
|
if (data.performedAt() != null && data.performedAt().isAfter(LocalDateTime.now())) {
|
||||||
|
response.addMessage("Cannot submit an exercise from the future.");
|
||||||
|
}
|
||||||
|
if (data.performedAt() != null && data.performedAt().isBefore(cutoff)) {
|
||||||
|
response.addMessage("Cannot submit an exercise too far in the past.");
|
||||||
|
}
|
||||||
|
if (data.reps() < 1 || data.reps() > 500) {
|
||||||
|
response.addMessage("Invalid rep count.");
|
||||||
|
}
|
||||||
|
BigDecimal rawWeight = BigDecimal.valueOf(data.weight());
|
||||||
|
WeightUnit weightUnit = WeightUnit.parse(data.weightUnit());
|
||||||
|
BigDecimal metricWeight = WeightUnit.toKilograms(rawWeight, weightUnit);
|
||||||
|
|
||||||
|
if (metricWeight.compareTo(BigDecimal.ZERO) <= 0 || metricWeight.compareTo(BigDecimal.valueOf(1000.0)) > 0) {
|
||||||
|
response.addMessage("Invalid weight.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
UploadsClient.FileMetadataResponse metadata = cdnClient.uploads.getFileMetadata(data.videoFileId());
|
||||||
|
if (metadata == null) {
|
||||||
|
response.addMessage("Missing video file.");
|
||||||
|
} else if (!metadata.availableForDownload()) {
|
||||||
|
response.addMessage("File not yet available for download.");
|
||||||
|
} else if (!"video/mp4".equals(metadata.mimeType())) {
|
||||||
|
response.addMessage("Invalid video file format.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error fetching file metadata.", e);
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error fetching uploaded video file metadata.");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ import { api } from 'src/api/main/index';
|
||||||
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
|
import { getGymCompoundId, GymRoutable } from 'src/router/gym-routing';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import {User} from 'src/api/main/auth';
|
import {User} from 'src/api/main/auth';
|
||||||
|
import {AuthStoreType} from 'stores/auth-store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data that's sent when creating a submission.
|
* The data that's sent when creating a submission.
|
||||||
*/
|
*/
|
||||||
export interface ExerciseSubmissionPayload {
|
export interface ExerciseSubmissionPayload {
|
||||||
name: string;
|
|
||||||
exerciseShortName: string;
|
exerciseShortName: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
weightUnit: string;
|
weightUnit: string;
|
||||||
|
@ -59,10 +59,11 @@ class SubmissionsModule {
|
||||||
|
|
||||||
public async createSubmission(
|
public async createSubmission(
|
||||||
gym: GymRoutable,
|
gym: GymRoutable,
|
||||||
payload: ExerciseSubmissionPayload
|
payload: ExerciseSubmissionPayload,
|
||||||
|
authStore: AuthStoreType
|
||||||
): Promise<ExerciseSubmission> {
|
): Promise<ExerciseSubmission> {
|
||||||
const gymId = getGymCompoundId(gym);
|
const gymId = getGymCompoundId(gym);
|
||||||
const response = await api.post(`/gyms/${gymId}/submissions`, payload);
|
const response = await api.post(`/gyms/${gymId}/submissions`, payload, authStore.axiosConfig);
|
||||||
return parseSubmission(response.data);
|
return parseSubmission(response.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,13 +31,18 @@ export default {
|
||||||
recentLifts: 'Recent Lifts',
|
recentLifts: 'Recent Lifts',
|
||||||
},
|
},
|
||||||
submitPage: {
|
submitPage: {
|
||||||
name: 'Your Name',
|
loginToSubmit: 'Login or register to submit your lift',
|
||||||
exercise: 'Exercise',
|
exercise: 'Exercise',
|
||||||
weight: 'Weight',
|
weight: 'Weight',
|
||||||
reps: 'Repetitions',
|
reps: 'Repetitions',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
upload: 'Video File to Upload',
|
upload: 'Video File to Upload',
|
||||||
submit: 'Submit',
|
submit: 'Submit',
|
||||||
|
submitUploading: 'Uploading video...',
|
||||||
|
submitCreatingSubmission: 'Creating submission...',
|
||||||
|
submitVideoProcessing: 'Processing...',
|
||||||
|
submitComplete: 'Submission complete!',
|
||||||
|
submitFailed: 'Submission processing failed. Please try again later.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
userPage: {
|
userPage: {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
||||||
recentLifts: 'Recente liften',
|
recentLifts: 'Recente liften',
|
||||||
},
|
},
|
||||||
submitPage: {
|
submitPage: {
|
||||||
name: 'Jouw naam',
|
loginToSubmit: 'Log in of meld je aan om je lift te indienen',
|
||||||
exercise: 'Oefening',
|
exercise: 'Oefening',
|
||||||
weight: 'Gewicht',
|
weight: 'Gewicht',
|
||||||
reps: 'Repetities',
|
reps: 'Repetities',
|
||||||
|
|
|
@ -11,17 +11,10 @@ A high-level overview of the submission process is as follows:
|
||||||
5. We wait on the submission page until the submission is done processing, then show a message and navigate to the submission page.
|
5. We wait on the submission page until the submission is done processing, then show a message and navigate to the submission page.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<q-page v-if="gym">
|
<q-page v-if="gym && authStore.loggedIn">
|
||||||
<!-- The below form contains the fields that will become part of the submission. -->
|
<!-- The below form contains the fields that will become part of the submission. -->
|
||||||
<q-form @submit="onSubmitted">
|
<q-form @submit="onSubmitted">
|
||||||
<SlimForm>
|
<SlimForm>
|
||||||
<div class="row">
|
|
||||||
<q-input
|
|
||||||
:label="$t('gymPage.submitPage.name')"
|
|
||||||
v-model="submissionModel.name"
|
|
||||||
class="col-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<q-select
|
<q-select
|
||||||
:options="exerciseOptions"
|
:options="exerciseOptions"
|
||||||
|
@ -76,57 +69,66 @@ A high-level overview of the submission process is as follows:
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<q-btn
|
<q-btn
|
||||||
:label="$t('gymPage.submitPage.submit')"
|
:label="submitButtonLabel"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="q-mt-md col-12"
|
class="q-mt-md col-12"
|
||||||
:disable="!submitButtonEnabled()"
|
:disable="!submitButtonEnabled()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row text-center" v-if="infoMessage">
|
|
||||||
<p>{{ infoMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</SlimForm>
|
</SlimForm>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|
||||||
|
<!-- If the user is not logged in, show a link to log in. -->
|
||||||
|
<q-page v-if="!authStore.loggedIn">
|
||||||
|
<div class="q-mt-lg text-center">
|
||||||
|
<router-link :to="`/login?next=${route.fullPath}`" class="text-primary">Login or register to submit your lift</router-link>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, Ref } from 'vue';
|
import {onMounted, ref, Ref} from 'vue';
|
||||||
import { getGymFromRoute, getGymRoute } from 'src/router/gym-routing';
|
import {getGymFromRoute} from 'src/router/gym-routing';
|
||||||
import SlimForm from 'components/SlimForm.vue';
|
import SlimForm from 'components/SlimForm.vue';
|
||||||
import api from 'src/api/main';
|
import api from 'src/api/main';
|
||||||
import { Gym } from 'src/api/main/gyms';
|
import {Gym} from 'src/api/main/gyms';
|
||||||
import { Exercise } from 'src/api/main/exercises';
|
import {Exercise} from 'src/api/main/exercises';
|
||||||
import { useRouter } from 'vue-router';
|
import {useRoute, useRouter} from 'vue-router';
|
||||||
import { sleep } from 'src/utils';
|
import {showApiErrorToast, sleep} from 'src/utils';
|
||||||
import { uploadVideoToCDN, VideoProcessingStatus, waitUntilVideoProcessingComplete } from 'src/api/cdn';
|
import {uploadVideoToCDN, VideoProcessingStatus, waitUntilVideoProcessingComplete} from 'src/api/cdn';
|
||||||
|
import {useAuthStore} from 'stores/auth-store';
|
||||||
|
import {useI18n} from 'vue-i18n';
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const gym: Ref<Gym | undefined> = ref<Gym>();
|
const gym: Ref<Gym | undefined> = ref<Gym>();
|
||||||
const exercises: Ref<Array<Exercise> | undefined> = ref<Array<Exercise>>();
|
const exercises: Ref<Array<Exercise> | undefined> = ref<Array<Exercise>>();
|
||||||
const exerciseOptions: Ref<Array<Option>> = ref([]);
|
const exerciseOptions: Ref<Array<Option>> = ref([]);
|
||||||
let submissionModel = ref({
|
let submissionModel = ref({
|
||||||
name: '',
|
|
||||||
exerciseShortName: '',
|
exerciseShortName: '',
|
||||||
weight: 100,
|
weight: 100,
|
||||||
weightUnit: 'Kg',
|
weightUnit: 'Kg',
|
||||||
reps: 1,
|
reps: 1,
|
||||||
videoFileId: '',
|
videoFileId: '',
|
||||||
videoFile: null,
|
|
||||||
date: new Date().toLocaleDateString('en-CA'),
|
date: new Date().toLocaleDateString('en-CA'),
|
||||||
});
|
});
|
||||||
const selectedVideoFile: Ref<File | undefined> = ref<File>();
|
const selectedVideoFile: Ref<File | undefined> = ref<File>();
|
||||||
const weightUnits = ['KG', 'LBS'];
|
const weightUnits = ['KG', 'LBS'];
|
||||||
|
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const infoMessage: Ref<string | undefined> = ref();
|
const submitButtonLabel = ref(i18n.t('gymPage.submitPage.submit'));
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -154,30 +156,59 @@ function validateForm() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs through the entire submission process.
|
||||||
|
*/
|
||||||
async function onSubmitted() {
|
async function onSubmitted() {
|
||||||
if (!selectedVideoFile.value || !gym.value) throw new Error('Invalid state.');
|
if (!selectedVideoFile.value || !gym.value) return;
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
infoMessage.value = 'Uploading video...';
|
// 1. Upload the video to the CDN.
|
||||||
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploading');
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
submissionModel.value.videoFileId = await uploadVideoToCDN(selectedVideoFile.value);
|
submissionModel.value.videoFileId = await uploadVideoToCDN(selectedVideoFile.value);
|
||||||
infoMessage.value = 'Creating submission...';
|
|
||||||
|
// 2. Wait for the video to be processed.
|
||||||
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitVideoProcessing');
|
||||||
|
const processingStatus = await waitUntilVideoProcessingComplete(submissionModel.value.videoFileId);
|
||||||
|
|
||||||
|
// 3. If successful upload, create the submission.
|
||||||
|
if (processingStatus === VideoProcessingStatus.COMPLETED) {
|
||||||
|
try {
|
||||||
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitCreatingSubmission');
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
const submission = await api.gyms.submissions.createSubmission(
|
const submission = await api.gyms.submissions.createSubmission(
|
||||||
gym.value,
|
gym.value,
|
||||||
submissionModel.value
|
submissionModel.value,
|
||||||
|
authStore
|
||||||
);
|
);
|
||||||
infoMessage.value = 'Submission processing...';
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitComplete');
|
||||||
const finalStatus = await waitUntilVideoProcessingComplete(submission.videoFileId);
|
await sleep(2000);
|
||||||
if (finalStatus === VideoProcessingStatus.COMPLETED) {
|
await router.push(`/submissions/${submission.id}`);
|
||||||
infoMessage.value = 'Submission complete!';
|
} catch (error: any) {
|
||||||
await sleep(1000);
|
if (error.response && error.response.status === 400) {
|
||||||
await router.push(getGymRoute(gym.value));
|
quasar.notify({
|
||||||
|
message: error.response.data.message,
|
||||||
|
type: 'warning',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
|
||||||
|
await sleep(3000);
|
||||||
} else {
|
} else {
|
||||||
infoMessage.value = 'Submission processing failed. Please try again later.';
|
showApiErrorToast(i18n, quasar);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, report the failed submission and give up.
|
||||||
|
} else {
|
||||||
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
|
||||||
|
await sleep(3000);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showApiErrorToast(i18n, quasar);
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
|
submitButtonLabel.value = i18n.t('gymPage.submitPage.submit');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -23,7 +23,9 @@ public class WeightedWildcardQueryBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Query> build(String rawSearchQuery) {
|
public Optional<Query> build(String rawSearchQuery) {
|
||||||
if (rawSearchQuery == null || rawSearchQuery.isBlank()) return Optional.empty();
|
if (rawSearchQuery == null) return Optional.empty();
|
||||||
|
rawSearchQuery = rawSearchQuery.replaceAll("\\*", "");
|
||||||
|
if (rawSearchQuery.isBlank()) return Optional.empty();
|
||||||
String[] terms = rawSearchQuery.toLowerCase().split("\\s+");
|
String[] terms = rawSearchQuery.toLowerCase().split("\\s+");
|
||||||
for (String term : terms) {
|
for (String term : terms) {
|
||||||
String searchTerm = term + "*";
|
String searchTerm = term + "*";
|
||||||
|
|
Loading…
Reference in New Issue