Email+username validations + Strava callback view text + edit profile now uses unified modal

[backend] add get user by email crud and route logic
[frontend] add getUserByUsername logic and changed current to getUserContainsUsername
[frontend] add text to Strava callback view
[frontend] added username and email form validations to add and edit logic
[frontend] profile edit logic now uses unified modal
This commit is contained in:
João Vitória Silva
2025-01-29 22:31:47 +00:00
parent 731ae98201
commit 97b8a4bcc9
18 changed files with 242 additions and 278 deletions

View File

@@ -162,6 +162,32 @@ def get_user_by_username(username: str, db: Session):
) from err
def get_user_by_email(email: str, db: Session):
try:
# Get the user from the database
user = (
db.query(users_models.User).filter(users_models.User.email == email).first()
)
# If the user was not found, return None
if user is None:
return None
# Format the birthdate
user = users_utils.format_user_birthdate(user)
# Return the user
return user
except Exception as err:
# Log the exception
core_logger.print_to_log(f"Error in get_user_by_email: {err}", "error", exc=err)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error",
) from err
def get_user_by_id(user_id: int, db: Session):
try:
# Get the user from the database
@@ -247,7 +273,7 @@ def create_user(user: users_schema.UserCreate, db: Session):
# Create a new user
db_user = users_models.User(
**user.model_dump(exclude={"password"}),
password=session_security.hash_password(user.password)
password=session_security.hash_password(user.password),
)
# Add the user to the database

View File

@@ -91,7 +91,25 @@ async def read_users_username(
],
):
# Get the user from the database by username
return users_crud.get_user_by_username(username=username, db=db)
return users_crud.get_user_by_username(username, db)
@router.get(
"/email/{email}",
response_model=users_schema.User | None,
)
async def read_users_email(
email: str,
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(core_database.get_db),
],
):
# Get the users from the database by email
return users_crud.get_user_by_email(email, db)
@router.get("/id/{user_id}", response_model=users_schema.User)

View File

@@ -5,6 +5,8 @@ files:
translation: /frontend/app/src/i18n/%two_letters_code%/components/%file_name%.%file_extension%
- source: /frontend/app/src/i18n/us/gears/*.json
translation: /frontend/app/src/i18n/%two_letters_code%/gears/%file_name%.%file_extension%
- source: /frontend/app/src/i18n/us/strava/*.json
translation: /frontend/app/src/i18n/%two_letters_code%/strava/%file_name%.%file_extension%
- source: /frontend/app/src/i18n/us/components/activities/*.json
translation: /frontend/app/src/i18n/%two_letters_code%/components/activities/%original_file_name%
- source: /frontend/app/src/i18n/us/components/activities/modals/*.json

View File

@@ -68,7 +68,7 @@ export default {
return;
}
try {
searchResults.value = await users.getUserByUsername(query);
searchResults.value = await users.getUserContainsUsername(query);
} catch (error) {
push.error(`${t('generalItems.errorFetchingInfo')} - ${error}`)
}

View File

@@ -32,73 +32,7 @@
<a class="mt-2 w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#editProfileModal">{{ $t("settingsUserProfileZone.buttonEditProfile") }}</a>
<!-- Modal edit user -->
<div class="modal fade" id="editProfileModal" tabindex="-1" aria-labelledby="editProfileModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="editProfileModal">{{ $t("usersListComponent.modalEditUserTitle") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form @submit.prevent="submitEditUserForm">
<div class="modal-body">
<label for="userImgEdit"><b>{{ $t("settingsUsersZone.addUserModalUserPhotoLabel") }}</b></label>
<div>
<div class="row">
<div class="col">
<input class="form-control" type="file" accept="image/*" name="userImgEdit" id="userImgEdit" @change="handleFileChange">
</div>
<div class="col" v-if="authStore.user.photo_path">
<a class="w-100 btn btn-danger" data-bs-dismiss="modal" @click="submitDeleteUserPhoto">{{ $t("usersListComponent.modalEditUserDeleteUserPhotoButton") }}</a>
</div>
</div>
</div>
<!-- username fields -->
<label for="userUsernameEdit"><b>* {{ $t("settingsUsersZone.addUserModalUsernameLabel") }}</b></label>
<input class="form-control" type="text" name="userUsernameEdit" :placeholder='$t("settingsUsersZone.addUserModalUsernamePlaceholder")' maxlength="250" v-model="editUserUsername" required>
<!-- name fields -->
<label for="userNameEdit"><b>* {{ $t("settingsUsersZone.addUserModalNameLabel") }}</b></label>
<input class="form-control" type="text" name="userNameEdit" :placeholder='$t("settingsUsersZone.addUserModalNamePlaceholder")' maxlength="250" v-model="editUserName" required>
<!-- email fields -->
<label for="userEmailEdit"><b>* {{ $t("settingsUsersZone.addUserModalEmailLabel") }}</b></label>
<input class="form-control" type="text" name="userEmailEdit" :placeholder='$t("settingsUsersZone.addUserModalEmailPlaceholder")' maxlength="45" v-model="editUserEmail" required>
<!-- city fields -->
<label for="userCityEdit"><b>{{ $t("settingsUsersZone.addUserModalTownLabel") }}</b></label>
<input class="form-control" type="text" name="userCityEdit" :placeholder='$t("settingsUsersZone.addUserModalTownPlaceholder")' maxlength="45" v-model="editUserTown">
<!-- birth date fields -->
<label for="userBirthDateEdit"><b>{{ $t("settingsUsersZone.addUserModalBirthdayLabel") }}</b></label>
<input class="form-control" type="date" name="userBirthDateEdit" v-model="editUserBirthdate">
<!-- gender fields -->
<label for="userGenderEdit"><b>* {{ $t("settingsUsersZone.addUserModalGenderLabel") }}</b></label>
<select class="form-control" name="userGenderEdit" v-model="editUserGender" required>
<option value="1">{{ $t("settingsUsersZone.addUserModalGenderOption1") }}</option>
<option value="2">{{ $t("settingsUsersZone.addUserModalGenderOption2") }}</option>
</select>
<!-- units fields -->
<label for="userUnitsEdit"><b>* {{ $t("settingsUsersZone.addUserModalUnitsLabel") }}</b></label>
<select class="form-control" name="userUnitsEdit" v-model="editUserUnits" required>
<option value="1">{{ $t("settingsUsersZone.addUserModalUnitsOption1") }}</option>
<option value="2">{{ $t("settingsUsersZone.addUserModalUnitsOption2") }}</option>
</select>
<!-- height fields -->
<label for="userHeightEdit"><b>{{ $t("settingsUsersZone.addUserModalHeightLabel") }} (cm)</b></label>
<input class="form-control" type="number" name="userHeightEdit" :placeholder='$t("settingsUsersZone.addUserModalHeightPlaceholder") + " (cm)"' v-model="editUserHeight">
<!-- preferred language fields -->
<label for="userPreferredLanguageEdit"><b>* {{ $t("settingsUsersZone.addUserModalUserPreferredLanguageLabel") }}</b></label>
<select class="form-control" name="userPreferredLanguageEdit" v-model="editUserPreferredLanguage" required>
<option value="us">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption1") }}</option>
<option value="ca">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption2") }}</option>
<option value="pt">{{ $t("settingsUsersZone.addUserModalPreferredLanguageOption3") }}</option>
</select>
<p>* {{ $t("generalItems.requiredField") }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItems.buttonClose") }}</button>
<button type="submit" class="btn btn-success" name="userEdit" data-bs-dismiss="modal">{{ $t("usersListComponent.modalEditUserTitle") }}</button>
</div>
</form>
</div>
</div>
</div>
<UsersAddEditUserModalComponent :action="'profile'" :user="authStore.user"/>
</div>
<div class="col">
<!-- user name -->
@@ -164,7 +98,6 @@
</template>
<script>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
// Importing the services
import { profile } from "@/services/profileService";
@@ -172,83 +105,18 @@ import { profile } from "@/services/profileService";
import { useAuthStore } from "@/stores/authStore";
// Import Notivue push
import { push } from "notivue";
// Import units utils
import { cmToFeetInches } from "@/utils/unitsUtils";
// Importing the components
import UserAvatarComponent from "../Users/UserAvatarComponent.vue";
import UsersAddEditUserModalComponent from "@/components/Settings/SettingsUsersZone/UsersAddEditUserModalComponent.vue";
export default {
components: {
UserAvatarComponent,
UsersAddEditUserModalComponent,
},
setup() {
const authStore = useAuthStore();
const { t, locale } = useI18n();
const editUserPhotoFile = ref(null);
const editUserUsername = ref(authStore.user.username);
const editUserName = ref(authStore.user.name);
const editUserEmail = ref(authStore.user.email);
const editUserTown = ref(authStore.user.city);
const editUserBirthdate = ref(authStore.user.birthdate);
const editUserGender = ref(authStore.user.gender);
const editUserUnits = ref(authStore.user.units);
const editUserHeight = ref(authStore.user.height);
const editUserPreferredLanguage = ref(authStore.user.preferred_language);
const editUserAccessType = ref(authStore.user.access_type);
const editUserPhotoPath = ref(authStore.user.photo_path);
const { feet, inches } = cmToFeetInches(authStore.user.height);
async function handleFileChange(event) {
editUserPhotoFile.value = event.target.files?.[0] ?? null;
}
async function submitEditUserForm() {
try {
if (editUserHeight.value === "" || editUserHeight.value === 0) {
editUserHeight.value = null;
}
const data = {
id: authStore.user.id,
username: editUserUsername.value,
name: editUserName.value,
email: editUserEmail.value,
city: editUserTown.value,
birthdate: editUserBirthdate.value,
gender: editUserGender.value,
units: editUserUnits.value,
height: editUserHeight.value,
preferred_language: editUserPreferredLanguage.value,
access_type: editUserAccessType.value,
photo_path: editUserPhotoPath.value,
is_active: 1,
};
// If there is a photo, upload it and get the photo url.
if (editUserPhotoFile.value) {
try {
data.photo_path = await profile.uploadProfileImage(
editUserPhotoFile.value,
);
} catch (error) {
// Set the error message
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);
}
}
await profile.editProfile(data);
// Save the user data in the local storage and in the store.
authStore.setUser(data, locale);
// Set the success message and show the success alert.
push.success(t("usersListComponent.userEditSuccessMessage"));
} catch (error) {
// If there is an error, set the error message and show the error alert.
push.error(
`${t("usersListComponent.userEditErrorMessage")} - ${error}`,
);
}
}
async function submitDeleteUserPhoto() {
try {
@@ -260,7 +128,7 @@ export default {
user.photo_path = null;
// Save the user data in the local storage and in the store.
authStore.setUser(user, locale);
authStore.setUser(user, authStore.session_id, locale);
// Set the success message and show the success alert.
push.success(t("usersListComponent.userPhotoDeleteSuccessMessage"));
@@ -275,21 +143,7 @@ export default {
return {
authStore,
t,
editUserUsername,
editUserName,
editUserEmail,
editUserTown,
editUserBirthdate,
editUserGender,
editUserUnits,
editUserHeight,
editUserPreferredLanguage,
editUserAccessType,
submitEditUserForm,
submitDeleteUserPhoto,
handleFileChange,
feet,
inches,
};
},
};

View File

@@ -100,7 +100,7 @@ export default {
}
try {
// Fetch the users based on the search username.
usersArray.value = await users.getUserByUsername(searchUsername.value);
usersArray.value = await users.getUserContainsUsername(searchUsername.value);
} catch (error) {
// If there is an error, set the error message and show the error alert.
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);

View File

@@ -1,11 +1,12 @@
<template>
<!-- Modal add/edit user -->
<div class="modal fade" :id="action == 'add' ? 'addUserModal' : (action == 'edit' ? editUserId : '')" tabindex="-1" :aria-labelledby="action == 'add' ? 'addUserModal' : (action == 'edit' ? editUserId : '')" aria-hidden="true">
<div class="modal fade" :id="action == 'add' ? 'addUserModal' : (action == 'edit' ? editUserModalId : (action == 'profile' ? 'editProfileModal' : ''))" tabindex="-1" :aria-labelledby="action == 'add' ? 'addUserModal' : (action == 'edit' ? editUserModalId : (action == 'profile' ? 'editProfileModal' : ''))" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addUserModal" v-if="action == 'add'">{{ $t("settingsUsersZone.buttonAddUser") }}</h1>
<h1 class="modal-title fs-5" id="editUserId" v-else>{{ $t("usersAddEditUserModalComponent.addEditUserModalEditTitle") }}</h1>
<h1 class="modal-title fs-5" id="editUserModalId" v-else-if="action == 'edit'">{{ $t("usersAddEditUserModalComponent.addEditUserModalEditTitle") }}</h1>
<h1 class="modal-title fs-5" id="editProfileModal" v-else>{{ $t("usersAddEditUserModalComponent.addEditUserModalEditProfileTitle") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form @submit.prevent="handleSubmit">
@@ -23,13 +24,22 @@
</div>
<!-- username fields -->
<label for="userUsernameAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalUsernameLabel") }}</b></label>
<input class="form-control" type="text" name="userUsernameAddEdit" :placeholder='$t("usersAddEditUserModalComponent.addEditUserModalUsernamePlaceholder")' maxlength="250" v-model="newEditUserUsername" required>
<input class="form-control" :class="{ 'is-invalid': !isUsernameExists }" type="text" name="userUsernameAddEdit" :placeholder='$t("usersAddEditUserModalComponent.addEditUserModalUsernamePlaceholder")' maxlength="250" v-model="newEditUserUsername" required>
<div id="validationUsernameFeedback" class="invalid-feedback" v-if="!isUsernameExists">
{{ $t("generalItems.errorUsernameAlreadyExistsFeedback") }}
</div>
<!-- name fields -->
<label for="userNameAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalNameLabel") }}</b></label>
<input class="form-control" type="text" name="userNameAddEdit" :placeholder='$t("usersAddEditUserModalComponent.addEditUserModalNamePlaceholder")' maxlength="250" v-model="newEditUserName" required>
<!-- email fields -->
<label for="userEmailAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalEmailLabel") }}</b></label>
<input class="form-control" type="text" name="userEmailAddEdit" :placeholder='$t("usersAddEditUserModalComponent.addEditUserModalEmailPlaceholder")' maxlength="45" v-model="newEditUserEmail" required>
<input class="form-control" :class="{ 'is-invalid': !isEmailValid || !isEmailExists }" type="text" name="userEmailAddEdit" :placeholder='$t("usersAddEditUserModalComponent.addEditUserModalEmailPlaceholder")' maxlength="45" v-model="newEditUserEmail" required>
<div id="validationEmailFeedback" class="invalid-feedback" v-if="!isEmailValid">
{{ $t("generalItems.errorEmailNotValidFeedback") }}
</div>
<div id="validationEmailFeedback" class="invalid-feedback" v-else-if="!isEmailExists">
{{ $t("generalItems.errorEmailAlreadyExistsFeedback") }}
</div>
<!-- password fields -->
<div v-if="action == 'add'">
<label for="passUserAdd"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalPasswordLabel") }}</b></label>
@@ -86,24 +96,29 @@
<option value="fr">{{ $t("usersAddEditUserModalComponent.addEditUserModalPreferredLanguageOption5") }}</option>
</select>
<!-- access type fields -->
<label for="userTypeAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalUserTypeLabel") }}</b></label>
<select class="form-control" name="userTypeAddEdit" v-model="newEditUserAccessType" required>
<option value="1">{{ $t("usersAddEditUserModalComponent.addEditUserModalUserTypeOption1") }}</option>
<option value="2">{{ $t("usersAddEditUserModalComponent.addEditUserModalUserTypeOption2") }}</option>
</select>
<div v-if="action != 'profile'">
<label for="userTypeAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalUserTypeLabel") }}</b></label>
<select class="form-control" name="userTypeAddEdit" v-model="newEditUserAccessType" required>
<option value="1">{{ $t("usersAddEditUserModalComponent.addEditUserModalUserTypeOption1") }}</option>
<option value="2">{{ $t("usersAddEditUserModalComponent.addEditUserModalUserTypeOption2") }}</option>
</select>
</div>
<!-- user is_active fields -->
<label for="userIsActiveAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalIsActiveLabel") }}</b></label>
<select class="form-control" name="userIsActiveAddEdit" v-model="newEditUserIsActive" required>
<option value="1">{{ $t("usersAddEditUserModalComponent.addEditUserModalIsActiveOption1") }}</option>
<option value="2">{{ $t("usersAddEditUserModalComponent.addEditUserModalIsActiveOption2") }}</option>
</select>
<div v-if="action != 'profile'">
<label for="userIsActiveAddEdit"><b>* {{ $t("usersAddEditUserModalComponent.addEditUserModalIsActiveLabel") }}</b></label>
<select class="form-control" name="userIsActiveAddEdit" v-model="newEditUserIsActive" required>
<option value="1">{{ $t("usersAddEditUserModalComponent.addEditUserModalIsActiveOption1") }}</option>
<option value="2">{{ $t("usersAddEditUserModalComponent.addEditUserModalIsActiveOption2") }}</option>
</select>
</div>
<p>* {{ $t("generalItems.requiredField") }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItems.buttonClose") }}</button>
<button type="submit" class="btn btn-success" name="userAdd" data-bs-dismiss="modal" v-if="action == 'add'" :disabled="!isPasswordValid">{{ $t("usersAddEditUserModalComponent.addEditUserModalAddTitle") }}</button>
<button type="submit" class="btn btn-success" name="userEdit" data-bs-dismiss="modal" v-else :disabled="!isFeetValid || !isInchesValid">{{ $t("usersAddEditUserModalComponent.addEditUserModalEditTitle") }}</button>
<button type="submit" class="btn btn-success" name="userAdd" data-bs-dismiss="modal" v-if="action == 'add'" :disabled="!isPasswordValid || !isUsernameExists || !isEmailValid || !isEmailExists">{{ $t("usersAddEditUserModalComponent.addEditUserModalAddTitle") }}</button>
<button type="submit" class="btn btn-success" name="userEdit" data-bs-dismiss="modal" v-else-if="action == 'edit'" :disabled="!isFeetValid || !isInchesValid || !isUsernameExists || !isEmailValid || !isEmailExists">{{ $t("usersAddEditUserModalComponent.addEditUserModalEditTitle") }}</button>
<button type="submit" class="btn btn-success" name="userEdit" data-bs-dismiss="modal" v-else :disabled="!isFeetValid || !isInchesValid || !isUsernameExists || !isEmailValid || !isEmailExists">{{ $t("usersAddEditUserModalComponent.addEditUserModalEditProfileTitle") }}</button>
</div>
</form>
</div>
@@ -112,13 +127,16 @@
</template>
<script>
import { ref, computed } from "vue";
import { ref, computed, watch } from "vue";
import { useI18n } from "vue-i18n";
// Import the stores
import { useAuthStore } from "@/stores/authStore";
// import lodash
import { debounce } from 'lodash';
// Import Notivue push
import { push } from "notivue";
// Importing the services
import { profile } from "@/services/profileService";
import { users } from "@/services/usersService";
// Import units utils
import { cmToFeetInches, feetAndInchesToCm } from "@/utils/unitsUtils";
@@ -139,7 +157,7 @@ export default {
const authStore = useAuthStore();
const { t, locale } = useI18n();
// edit user specific variables
const editUserId = ref("");
const editUserModalId = ref("");
// edit and add user variables
const newEditUserPhotoFile = ref(null);
const newEditUserUsername = ref("");
@@ -162,6 +180,71 @@ export default {
const newEditUserAccessType = ref(1);
const newEditUserIsActive = ref(1);
const newEditUserPhotoPath = ref(null);
const isUsernameExists = ref(true);
const validateUsernameExists = debounce(async () => {
let tryValidate = false;
if (props.action === 'edit' || props.action === 'profile') {
if (newEditUserUsername.value !== props.user.username) {
tryValidate = true;
}
} else {
if (props.action === 'add') {
if (newEditUserUsername.value !== "") {
tryValidate = true;
}
}
}
if (tryValidate) {
try {
if (await users.getUserByUsername(newEditUserUsername.value)) {
isUsernameExists.value = false;
} else {
isUsernameExists.value = true;
}
} catch (error) {
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);
}
} else {
isUsernameExists.value = true;
}
}, 500);
const isEmailExists = ref(true);
// Regular expression for email validation
const isEmailValid = computed(() => {
const emailRegex = /^[^\s@]{1,}@[^\s@]{2,}\.[^\s@]{2,}$/;
return emailRegex.test(newEditUserEmail.value);
});
const validateEmailExists = debounce(async () => {
let tryValidate = false;
if (props.action === 'edit' || props.action === 'profile') {
if (newEditUserEmail.value !== props.user.email) {
tryValidate = true;
}
} else {
if (props.action === 'add') {
if (newEditUserEmail.value !== "") {
tryValidate = true;
}
}
}
if (tryValidate) {
if (isEmailValid.value) {
try {
if (await users.getUserByEmail(newEditUserEmail.value)) {
isEmailExists.value = false;
} else {
isEmailExists.value = true;
}
} catch (error) {
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);
}
}
} else {
isEmailExists.value = true;
}
}, 500);
// new user variables
const newUserPassword = ref("");
const isPasswordValid = computed(() => {
@@ -171,7 +254,9 @@ export default {
});
if (props.user) {
editUserId.value = `editUserModal${props.user.id}`;
if (props.action === 'edit') {
editUserModalId.value = `editUserModal${props.user.id}`;
}
newEditUserUsername.value = props.user.username;
newEditUserName.value = props.user.name;
newEditUserEmail.value = props.user.email;
@@ -282,24 +367,36 @@ export default {
is_active: newEditUserIsActive.value,
};
await users.editUser(data.id, data);
// If there is a photo, upload it and get the photo url.
if (newEditUserPhotoFile.value) {
try {
if (props.action === 'profile') {
data.photo_path = await profile.uploadProfileImage(
newEditUserPhotoFile.value,
);
} else {
data.photo_path = await users.uploadImage(
newEditUserPhotoFile.value,
data.id,
);
}
} catch (error) {
// Set the error message
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);
}
}
// If there is a photo, upload it and get the photo url.
if (newEditUserPhotoFile.value) {
try {
data.photo_path = await users.uploadImage(
newEditUserPhotoFile.value,
data.id,
);
} catch (error) {
// Set the error message
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);
}
}
if (props.action === 'profile') {
await profile.editProfile(data);
} else {
await users.editUser(data.id, data);
}
emit("editedUser", data);
if (props.action === 'edit') {
emit("editedUser", data);
}
if (data.id === authStore.user.id) {
if (data.id === authStore.user.id || props.action === 'profile') {
authStore.setUser(data, authStore.session_id, locale);
}
@@ -334,10 +431,17 @@ export default {
}
}
// Watchers
// Watch the newEditUserUsername variable.
watch(newEditUserUsername, validateUsernameExists, { immediate: false });
// Watch the newEditUserEmail variable.
watch(newEditUserEmail, validateEmailExists, { immediate: false });
return {
authStore,
t,
editUserId,
editUserModalId,
newEditUserPhotoFile,
newEditUserUsername,
newEditUserName,
@@ -354,6 +458,9 @@ export default {
newEditUserIsActive,
newEditUserPhotoPath,
newUserPassword,
isUsernameExists,
isEmailExists,
isEmailValid,
isPasswordValid,
isFeetValid,
isInchesValid,

View File

@@ -99,83 +99,10 @@ export default {
const { t } = useI18n();
const authStore = useAuthStore();
const userProp = ref(props.user);
const editUserPhotoFile = ref(null);
const editUserUsername = ref(userProp.value.username);
const editUserName = ref(userProp.value.name);
const editUserEmail = ref(userProp.value.email);
const editUserTown = ref(userProp.value.city);
const editUserBirthdate = ref(userProp.value.birthdate);
const editUserGender = ref(userProp.value.gender);
const editUserUnits = ref(userProp.value.units);
const editUserHeight = ref(userProp.value.height);
const editUserPreferredLanguage = ref(userProp.value.preferred_language);
const editUserAccessType = ref(userProp.value.access_type);
const editUserIsActive = ref(userProp.value.is_active);
const userDetails = ref(false);
const userSessions = ref([]);
const isLoading = ref(true);
async function handleFileChange(event) {
editUserPhotoFile.value = event.target.files?.[0] ?? null;
}
async function submitEditUserForm() {
try {
const data = {
id: userProp.value.id,
username: editUserUsername.value,
name: editUserName.value,
email: editUserEmail.value,
city: editUserTown.value,
birthdate: editUserBirthdate.value,
gender: editUserGender.value,
units: editUserUnits.value,
height: editUserHeight.value,
preferred_language: editUserPreferredLanguage.value,
access_type: editUserAccessType.value,
photo_path: null,
is_active: editUserIsActive.value,
};
await users.editUser(userProp.value.id, data);
// If there is a photo, upload it and get the photo url.
if (editUserPhotoFile.value) {
try {
userProp.value.photo_path = await users.uploadImage(
editUserPhotoFile.value,
userProp.value.id,
);
} catch (error) {
// Set the error message
push.error(`${t("generalItems.errorFetchingInfo")} - ${error}`);
}
}
userProp.value.username = editUserUsername.value;
userProp.value.name = editUserName.value;
userProp.value.email = editUserEmail.value;
userProp.value.city = editUserTown.value;
userProp.value.birthdate = editUserBirthdate.value;
userProp.value.city = editUserTown.value;
userProp.value.birthdate = editUserBirthdate.value;
userProp.value.gender = editUserGender.value;
userProp.value.units = editUserUnits.value;
userProp.value.height = editUserHeight.value;
userProp.value.preferred_language = editUserPreferredLanguage.value;
userProp.value.access_type = editUserAccessType.value;
userProp.value.is_active = editUserIsActive.value;
// Set the success message and show the success alert.
push.success(t("usersListComponent.userEditSuccessMessage"));
} catch (error) {
// If there is an error, set the error message and show the error alert.
push.error(
`${t("usersListComponent.userEditErrorMessage")} - ${error}`,
);
}
}
async function submitDeleteUser() {
try {
await users.deleteUser(userProp.value.id);
@@ -245,19 +172,6 @@ export default {
return {
t,
authStore,
editUserUsername,
editUserName,
editUserEmail,
editUserTown,
editUserBirthdate,
editUserGender,
editUserUnits,
editUserHeight,
editUserPreferredLanguage,
editUserAccessType,
editUserIsActive,
submitEditUserForm,
handleFileChange,
submitDeleteUser,
submitDeleteUserPhoto,
showUserDetails,

View File

@@ -0,0 +1,4 @@
{
"stravaCallbackViewTitle1": "Handling Strava callback",
"stravaCallbackViewTitle2": "Please wait while Strava is being linked to your account"
}

View File

@@ -0,0 +1,4 @@
{
"stravaCallbackViewTitle1": "Handling Strava callback",
"stravaCallbackViewTitle2": "Please wait while Strava is being linked to your account"
}

View File

@@ -0,0 +1,4 @@
{
"stravaCallbackViewTitle1": "Handling Strava callback",
"stravaCallbackViewTitle2": "Please wait while Strava is being linked to your account"
}

View File

@@ -38,6 +38,7 @@ import caHomeView from './ca/homeView.json';
import caLoginView from './ca/loginView.json';
import caGearsView from './ca/gears/gearsView.json';
import caGearView from './ca/gears/gearView.json';
import caStravaCallbackView from './ca/strava/stravaCallbackView.json';
import caActivityView from './ca/activityView.json';
import caHealthView from './ca/healthView.json';
import caNotFoundView from './ca/notFoundView.json';
@@ -81,6 +82,7 @@ import deHomeView from './de/homeView.json';
import deLoginView from './de/loginView.json';
import deGearsView from './de/gears/gearsView.json';
import deGearView from './de/gears/gearView.json';
import deStravaCallbackView from './de/strava/stravaCallbackView.json';
import deActivityView from './de/activityView.json';
import deHealthView from './de/healthView.json';
import deNotFoundView from './de/notFoundView.json';
@@ -125,6 +127,7 @@ import frHomeView from './fr/homeView.json';
import frLoginView from './fr/loginView.json';
import frGearsView from './fr/gears/gearsView.json';
import frGearView from './fr/gears/gearView.json';
import frStravaCallbackView from './fr/strava/stravaCallbackView.json';
import frActivityView from './fr/activityView.json';
import frHealthView from './fr/healthView.json';
import frNotFoundView from './fr/notFoundView.json';
@@ -169,6 +172,7 @@ import ptHomeView from './pt/homeView.json';
import ptLoginView from './pt/loginView.json';
import ptGearsView from './pt/gears/gearsView.json';
import ptGearView from './pt/gears/gearView.json';
import ptStravaCallbackView from './pt/strava/stravaCallbackView.json';
import ptActivityView from './pt/activityView.json';
import ptHealthView from './pt/healthView.json';
import ptNotFoundView from './pt/notFoundView.json';
@@ -212,6 +216,7 @@ import usHomeView from './us/homeView.json';
import usLoginView from './us/loginView.json';
import usGearsView from './us/gears/gearsView.json';
import usGearView from './us/gears/gearView.json';
import usStravaCallbackView from './us/strava/stravaCallbackView.json';
import usActivityView from './us/activityView.json';
import usHealthView from './us/healthView.json';
import usNotFoundView from './us/notFoundView.json';
@@ -257,6 +262,7 @@ const messages = {
loginView: caLoginView,
gearsView: caGearsView,
gearView: caGearView,
stravaCallbackView: caStravaCallbackView,
activity: caActivityView,
healthView: caHealthView,
notFound: caNotFoundView,
@@ -300,6 +306,7 @@ const messages = {
loginView: deLoginView,
gearsView: deGearsView,
gearView: deGearView,
stravaCallbackView: deStravaCallbackView,
activity: deActivityView,
healthView: deHealthView,
notFound: deNotFoundView,
@@ -343,6 +350,7 @@ const messages = {
loginView: frLoginView,
gearsView: frGearsView,
gearView: frGearView,
stravaCallbackView: frStravaCallbackView,
activity: frActivityView,
healthView: frHealthView,
notFound: frNotFoundView,
@@ -386,6 +394,7 @@ const messages = {
loginView: ptLoginView,
gearsView: ptGearsView,
gearView: ptGearView,
stravaCallbackView: ptStravaCallbackView,
activity: ptActivityView,
healthView: ptHealthView,
notFound: ptNotFoundView,
@@ -429,6 +438,7 @@ const messages = {
loginView: usLoginView,
gearsView: usGearsView,
gearView: usGearView,
stravaCallbackView: usStravaCallbackView,
activity: usActivityView,
healthView: usHealthView,
notFound: usNotFoundView,

View File

@@ -0,0 +1,4 @@
{
"stravaCallbackViewTitle1": "A processar retorno da chamada de Strava",
"stravaCallbackViewTitle2": "Por favor aguarde enquanto Strava está a ser ligado à sua conta. Não atualize esta página."
}

View File

@@ -1,5 +1,6 @@
{
"addEditUserModalEditTitle": "Edit user",
"addEditUserModalEditProfileTitle": "Edit profile",
"addEditUserModalAddTitle": "Add user",
"addEditUserModalDeleteUserPhotoButton": "Delete photo",
"addEditUserModalUserPhotoLabel": "User photo",

View File

@@ -6,6 +6,9 @@
"errorFetchingInfo": "Error fetching info",
"errorEditingInfo": "Error editing info",
"errorDeletingInfo": "Error deleting info",
"errorUsernameAlreadyExistsFeedback": "Username already exists",
"errorEmailNotValidFeedback": "Email not valid",
"errorEmailAlreadyExistsFeedback": "Email already exists",
"unitsCm": "cm",
"unitsCms": "cms",
"unitsInches": "inches",

View File

@@ -0,0 +1,4 @@
{
"stravaCallbackViewTitle1": "Handling Strava callback",
"stravaCallbackViewTitle2": "Please wait while Strava is being linked to your account. Do not refresh this page."
}

View File

@@ -10,9 +10,15 @@ export const users = {
getUserById(user_id) {
return fetchGetRequest(`users/id/${user_id}`);
},
getUserByUsername(username){
getUserContainsUsername(username){
return fetchGetRequest(`users/username/contains/${username}`);
},
getUserByUsername(username){
return fetchGetRequest(`users/username/${username}`);
},
getUserByEmail(email){
return fetchGetRequest(`users/email/${email}`);
},
createUser(data) {
return fetchPostRequest('users/create', data)
},

View File

@@ -1,11 +1,14 @@
<template>
<div>
<div class="text-center">
<LoadingComponent />
<br>
<p>{{ $t("stravaCallbackView.stravaCallbackViewTitle1") }}</p>
<p>{{ $t("stravaCallbackView.stravaCallbackViewTitle2") }}</p>
</div>
</template>
<script>
import { ref, onMounted } from "vue";
import { onMounted } from "vue";
import { useI18n } from "vue-i18n";
// Import Notivue push
import { push } from "notivue";