Added lots of stuff!!!

This commit is contained in:
Andrew Lalis 2023-02-17 17:05:15 +01:00
parent 97c69ea34d
commit ae3c22422c
21 changed files with 463 additions and 124 deletions

View File

@ -49,6 +49,7 @@ public class SecurityConfig {
"/submissions/**",
"/auth/reset-password",
"/auth/users/*",
"/auth/users/*/access",
"/auth/users/*/followers",
"/auth/users/*/following",
"/users/*/recent-submissions"

View File

@ -2,10 +2,10 @@ package nl.andrewlalis.gymboard_api.domains.api.controller;
import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(path = "/submissions")
@ -20,4 +20,10 @@ public class SubmissionController {
public SubmissionResponse getSubmission(@PathVariable String submissionId) {
return submissionService.getSubmission(submissionId);
}
@DeleteMapping(path = "/{submissionId}")
public ResponseEntity<Void> deleteSubmission(@PathVariable String submissionId, @AuthenticationPrincipal User user) {
submissionService.deleteSubmission(submissionId, user);
return ResponseEntity.noContent().build();
}
}

View File

@ -132,4 +132,15 @@ public class ExerciseSubmissionService {
}
return response;
}
@Transactional
public void deleteSubmission(String submissionId, User user) {
Submission submission = submissionRepository.findById(submissionId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!submission.getUser().getId().equals(user.getId())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete other user's submission.");
}
// TODO: Find a secure way to delete the associated video.
submissionRepository.delete(submission);
}
}

View File

@ -2,6 +2,7 @@ package nl.andrewlalis.gymboard_api.domains.auth.controller;
import nl.andrewlalis.gymboard_api.domains.auth.dto.*;
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import nl.andrewlalis.gymboard_api.domains.auth.service.UserAccessService;
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@ -12,9 +13,11 @@ import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
private final UserService userService;
private final UserAccessService userAccessService;
public UserController(UserService userService) {
public UserController(UserService userService, UserAccessService userAccessService) {
this.userService = userService;
this.userAccessService = userAccessService;
}
/**
@ -33,6 +36,11 @@ public class UserController {
return userService.getUser(userId);
}
@GetMapping(path = "/auth/users/{userId}/access")
public UserAccessResponse getUserAccess(@PathVariable String userId) {
return new UserAccessResponse(userAccessService.currentUserHasAccess(userId));
}
/**
* Endpoint for updating one's own password.
* @param user The user that's updating their password.
@ -71,6 +79,11 @@ public class UserController {
return userService.updatePreferences(user.getId(), payload);
}
@GetMapping(path = "/auth/users/{user1Id}/relationship-to/{user2Id}")
public UserRelationshipResponse getRelationship(@PathVariable String user1Id, @PathVariable String user2Id) {
return userService.getRelationship(user1Id, user2Id);
}
@PostMapping(path = "/auth/users/{userId}/followers")
public ResponseEntity<Void> followUser(@AuthenticationPrincipal User myUser, @PathVariable String userId) {
userService.followUser(myUser.getId(), userId);
@ -92,4 +105,14 @@ public class UserController {
public Page<UserResponse> getFollowing(@AuthenticationPrincipal User user, Pageable pageable) {
return userService.getFollowing(user.getId(), pageable);
}
@GetMapping(path = "/auth/users/{userId}/followers")
public Page<UserResponse> getUserFollowers(@PathVariable String userId, Pageable pageable) {
return userService.getFollowers(userId, pageable);
}
@GetMapping(path = "/auth/users/{userId}/following")
public Page<UserResponse> getUserFollowing(@PathVariable String userId, Pageable pageable) {
return userService.getFollowing(userId, pageable);
}
}

View File

@ -0,0 +1,3 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
public record UserAccessResponse(boolean accessible) {}

View File

@ -0,0 +1,6 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
public record UserRelationshipResponse(
boolean following,
boolean followedBy
) {}

View File

@ -36,6 +36,9 @@ public class UserAccessService {
if (targetUser != null && !targetUser.getPreferences().isAccountPrivate()) {
return true;
}
if (user != null && targetUser != null && user.getId().equals(targetUser.getId())) {
return true;
}
return user != null && followingRepository.existsByFollowedUserAndFollowingUser(targetUser, user);
}
@ -79,9 +82,10 @@ public class UserAccessService {
@Transactional(readOnly = true)
public boolean currentUserHasAccess(String userId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
User user = null;
if (auth instanceof TokenAuthentication tokenAuth) {
return userHasAccess(tokenAuth.user(), userId);
user = tokenAuth.user();
}
return false;
return userHasAccess(user, userId);
}
}

View File

@ -38,6 +38,7 @@ public class UserService {
private final UserActivationCodeRepository activationCodeRepository;
private final PasswordResetCodeRepository passwordResetCodeRepository;
private final UserFollowingRepository userFollowingRepository;
private final UserAccessService userAccessService;
private final ULID ulid;
private final PasswordEncoder passwordEncoder;
private final JavaMailSender mailSender;
@ -51,7 +52,7 @@ public class UserService {
UserPreferencesRepository userPreferencesRepository,
UserActivationCodeRepository activationCodeRepository,
PasswordResetCodeRepository passwordResetCodeRepository,
UserFollowingRepository userFollowingRepository, ULID ulid,
UserFollowingRepository userFollowingRepository, UserAccessService userAccessService, ULID ulid,
PasswordEncoder passwordEncoder,
JavaMailSender mailSender
) {
@ -61,6 +62,7 @@ public class UserService {
this.activationCodeRepository = activationCodeRepository;
this.passwordResetCodeRepository = passwordResetCodeRepository;
this.userFollowingRepository = userFollowingRepository;
this.userAccessService = userAccessService;
this.ulid = ulid;
this.passwordEncoder = passwordEncoder;
this.mailSender = mailSender;
@ -280,6 +282,7 @@ public class UserService {
@Transactional
public void followUser(String followerId, String followedId) {
if (followerId.equals(followedId)) return;
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
User followed = userRepository.findById(followedId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
@ -290,6 +293,7 @@ public class UserService {
@Transactional
public void unfollowUser(String followerId, String followedId) {
if (followerId.equals(followedId)) return;
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
User followed = userRepository.findById(followedId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
@ -300,6 +304,7 @@ public class UserService {
public Page<UserResponse> getFollowers(String userId, Pageable pageable) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
userAccessService.enforceUserAccess(user);
return userFollowingRepository.findAllByFollowedUserOrderByCreatedAtDesc(user, pageable)
.map(UserFollowing::getFollowingUser)
.map(UserResponse::new);
@ -309,8 +314,24 @@ public class UserService {
public Page<UserResponse> getFollowing(String userId, Pageable pageable) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
userAccessService.enforceUserAccess(user);
return userFollowingRepository.findAllByFollowingUserOrderByCreatedAtDesc(user, pageable)
.map(UserFollowing::getFollowedUser)
.map(UserResponse::new);
}
@Transactional(readOnly = true)
public UserRelationshipResponse getRelationship(String user1Id, String user2Id) {
User user1 = userRepository.findById(user1Id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
User user2 = userRepository.findById(user2Id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
userAccessService.enforceUserAccess(user1);
boolean user1FollowingUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user2, user1);
boolean user1FollowedByUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user1, user2);
return new UserRelationshipResponse(
user1FollowingUser2,
user1FollowedByUser2
);
}
}

View File

@ -17,3 +17,5 @@ spring.mail.properties.mail.smtp.timeout=10000
app.auth.private-key-location=./private_key.der
app.web-origin=http://localhost:9000
app.cdn-origin=http://localhost:8082
#logging.level.root=DEBUG

View File

@ -27,6 +27,11 @@ export interface UserPersonalDetails {
sex: PersonSex;
}
export interface UserRelationship {
following: boolean;
followedBy: boolean;
}
export interface UserPreferences {
userId: string;
accountPrivate: boolean;
@ -104,6 +109,11 @@ class AuthModule {
return response.data;
}
public async isUserAccessible(userId: string, authStore: AuthStoreType): Promise<boolean> {
const response = await api.get(`/auth/users/${userId}/access`, authStore.axiosConfig);
return response.data.accessible;
}
public async updatePassword(newPassword: string, authStore: AuthStoreType) {
await api.post(
'/auth/me/password',
@ -142,6 +152,29 @@ class AuthModule {
const response = await api.post('/auth/me/preferences', newPreferences, authStore.axiosConfig);
return response.data;
}
public async getFollowers(userId: string, authStore: AuthStoreType, page: number, count: number): Promise<User[]> {
const response = await api.get(`/auth/users/${userId}/followers?page=${page}&count=${count}`, authStore.axiosConfig);
return response.data.content;
}
public async getFollowing(userId: string, authStore: AuthStoreType, page: number, count: number): Promise<User[]> {
const response = await api.get(`/auth/users/${userId}/following?page=${page}&count=${count}`, authStore.axiosConfig);
return response.data.content;
}
public async getRelationshipTo(userId: string, targetUserId: string, authStore: AuthStoreType): Promise<UserRelationship> {
const response = await api.get(`/auth/users/${userId}/relationship-to/${targetUserId}`, authStore.axiosConfig);
return response.data;
}
public async followUser(userId: string, authStore: AuthStoreType) {
await api.post(`/auth/users/${userId}/followers`, undefined, authStore.axiosConfig);
}
public async unfollowUser(userId: string, authStore: AuthStoreType) {
await api.delete(`/auth/users/${userId}/followers`, authStore.axiosConfig);
}
}
export default AuthModule;

View File

@ -66,6 +66,10 @@ class SubmissionsModule {
const response = await api.post(`/gyms/${gymId}/submissions`, payload, authStore.axiosConfig);
return parseSubmission(response.data);
}
public async deleteSubmission(id: string, authStore: AuthStoreType) {
await api.delete(`/submissions/${id}`, authStore.axiosConfig);
}
}
export default SubmissionsModule;

View File

@ -0,0 +1,44 @@
<!--
A menu for a page, in which items refer to different sub-pages.
-->
<template>
<q-btn-group spread square push>
<q-btn
v-for="(item, index) in items" :key="index"
:label="item.label"
:to="getItemPath(item)"
:color="isItemSelected(index) ? 'primary' : 'secondary'"
/>
</q-btn-group>
</template>
<script setup lang="ts">
import {useRoute} from 'vue-router';
const route = useRoute();
interface MenuItem {
label: string;
to?: string;
}
interface Props {
items: MenuItem[];
baseRoute: string;
}
const props = defineProps<Props>();
function isItemSelected(index: number): boolean {
const item = props.items[index];
return route.path === getItemPath(item);
}
function getItemPath(item: MenuItem): string {
if (!item.to || item.to === '') return props.baseRoute;
return props.baseRoute + '/' + item.to;
}
</script>
<style scoped>
</style>

View File

@ -3,7 +3,7 @@
<q-item-section>
<q-item-label>{{ user.name }}</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-section side top v-if="user.submissionCount">
<q-badge color="primary" :label="submissionCountLabel"/>
</q-item-section>
</q-item>

View File

@ -20,6 +20,13 @@
<p>
{{ submission.performedAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
</p>
<!-- Deletion button is only visible if the user who submitted it is viewing it. -->
<q-btn
v-if="authStore.user && authStore.user.id === submission.user.id"
label="Delete"
@click="deleteSubmission"
/>
</StandardCenteredPage>
</q-page>
</template>
@ -33,11 +40,18 @@ import { useRoute, useRouter } from 'vue-router';
import { DateTime } from 'luxon';
import { getFileUrl } from 'src/api/cdn';
import { getGymRoute } from 'src/router/gym-routing';
import {useAuthStore} from "stores/auth-store";
import {showApiErrorToast} from "src/utils";
import {useI18n} from "vue-i18n";
import {useQuasar} from "quasar";
const submission: Ref<ExerciseSubmission | undefined> = ref();
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const i18n = useI18n();
const quasar = useQuasar();
onMounted(async () => {
const submissionId = route.params.submissionId as string;
@ -48,6 +62,18 @@ onMounted(async () => {
await router.push('/');
}
});
async function deleteSubmission() {
// TODO: Confirm via a dialog or something before deleting.
if (!submission.value) return;
try {
await api.gyms.submissions.deleteSubmission(submission.value.id, authStore);
await router.push('/');
} catch (error) {
console.error(error);
showApiErrorToast(i18n, quasar);
}
}
</script>
<style scoped>
.submission-video {

View File

@ -1,89 +0,0 @@
<template>
<q-page>
<StandardCenteredPage v-if="user">
<h3>{{ user?.name }}</h3>
<hr>
<div v-if="userPrivate">
This account is private.
</div>
<div v-if="recentSubmissions.length > 0">
<h4 class="text-center">{{ $t('userPage.recentLifts') }}</h4>
<q-list separator>
<ExerciseSubmissionListItem
v-for="sub in recentSubmissions"
:submission="sub"
:key="sub.id"
:show-name="false"
/>
</q-list>
</div>
</StandardCenteredPage>
<StandardCenteredPage v-if="userNotFound">
<h3>{{ $t('userPage.notFound.title') }}</h3>
<p>{{ $t('userPage.notFound.description') }}</p>
</StandardCenteredPage>
</q-page>
</template>
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import {onMounted, ref, Ref} from 'vue';
import {User} from 'src/api/main/auth';
import api from 'src/api/main';
import {useRoute} from 'vue-router';
import {useAuthStore} from 'stores/auth-store';
import {useI18n} from 'vue-i18n';
import {useQuasar} from 'quasar';
import {showApiErrorToast} from 'src/utils';
import {ExerciseSubmission} from 'src/api/main/submission';
import ExerciseSubmissionListItem from 'components/ExerciseSubmissionListItem.vue';
const route = useRoute();
const authStore = useAuthStore();
const i18n = useI18n();
const quasar = useQuasar();
/**
* The user that this page displays information about.
*/
const user: Ref<User | undefined> = ref();
const recentSubmissions: Ref<ExerciseSubmission[]> = ref([]);
const isOwnUser = ref(false);
const userNotFound = ref(false);
const userPrivate = ref(false);
onMounted(async () => {
const userId = route.params.userId as string;
try {
user.value = await api.auth.getUser(userId, authStore);
} catch (error: any) {
if (error.response && error.response.status === 404) {
userNotFound.value = true;
} else {
showApiErrorToast(i18n, quasar);
}
}
isOwnUser.value = authStore.loggedIn && user.value?.id === authStore.user?.id;
// If the user exists, try and fetch their latest submissions, and handle a 403 forbidden as private.
if (user.value) {
try {
recentSubmissions.value = await api.users.getRecentSubmissions(user.value?.id, authStore);
} catch (error: any) {
if (error.response && error.response.status === 403) {
userPrivate.value = true;
} else {
showApiErrorToast(i18n, quasar);
}
}
}
});
</script>
<style scoped>
</style>

View File

@ -2,23 +2,14 @@
<q-page>
<StandardCenteredPage v-if="gym">
<h3 class="q-my-md text-center">{{ gym.displayName }}</h3>
<q-btn-group spread square push>
<q-btn
:label="$t('gymPage.home')"
:to="getGymRoute(gym)"
:color="homePageSelected ? 'primary' : 'secondary'"
<PageMenu
:base-route="getGymRoute(gym)"
:items="[
{label: t('gymPage.home')},
{label: t('gymPage.submit'), to: 'submit'},
{label: t('gymPage.leaderboard'), to: 'leaderboard'}
]"
/>
<q-btn
:label="$t('gymPage.submit')"
:to="getGymRoute(gym) + '/submit'"
:color="submitPageSelected ? 'primary' : 'secondary'"
/>
<q-btn
:label="$t('gymPage.leaderboard')"
:to="getGymRoute(gym) + '/leaderboard'"
:color="leaderboardPageSelected ? 'primary' : 'secondary'"
/>
</q-btn-group>
<router-view />
</StandardCenteredPage>
</q-page>
@ -30,9 +21,12 @@ import { useRoute, useRouter } from 'vue-router';
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import { getGymFromRoute, getGymRoute } from 'src/router/gym-routing';
import { Gym } from 'src/api/main/gyms';
import PageMenu from "components/PageMenu.vue";
import {useI18n} from "vue-i18n";
const route = useRoute();
const router = useRouter();
const t = useI18n().t;
const gym: Ref<Gym | undefined> = ref<Gym>();

View File

@ -0,0 +1,34 @@
<template>
<q-list separator>
<q-item
v-for="user in followers" :key="user.id"
:to="`/users/${user.id}`"
>
<q-item-section>
<q-item-label>{{ user.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<script setup lang="ts">
import {User} from "src/api/main/auth";
import {useAuthStore} from "stores/auth-store";
import {onMounted, ref, Ref} from "vue";
import api from 'src/api/main';
interface Props {
user: User;
}
const props = defineProps<Props>();
const authStore = useAuthStore();
const followers: Ref<User[]> = ref([]);
onMounted(async () => {
followers.value = await api.auth.getFollowers(props.user.id, authStore, 0, 10);
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,34 @@
<template>
<q-list separator>
<q-item
v-for="user in following" :key="user.id"
:to="`/users/${user.id}`"
>
<q-item-section>
<q-item-label>{{ user.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<script setup lang="ts">
import {User} from "src/api/main/auth";
import {useAuthStore} from "stores/auth-store";
import {onMounted, ref, Ref} from "vue";
import api from 'src/api/main';
interface Props {
user: User;
}
const props = defineProps<Props>();
const authStore = useAuthStore();
const following: Ref<User[]> = ref([]);
onMounted(async () => {
following.value = await api.auth.getFollowing(props.user.id, authStore, 0, 10);
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,138 @@
<template>
<q-page>
<StandardCenteredPage v-if="user">
<h3>{{ user?.name }}</h3>
<div v-if="relationship">
<q-btn v-if="!relationship.following" label="Follow" @click="followUser"/>
<q-btn v-if="relationship.following" label="Unfollow" @click="unfollowUser"/>
</div>
<PageMenu
:base-route="`/users/${user.id}`"
:items="[
{label: 'Lifts', to: ''},
{label: 'Followers', to: 'followers'},
{label: 'Following', to: 'following'}
]"
/>
<!-- Sub-pages are rendered here. -->
<div v-if="userAccessible">
<UserSubmissionsPage :user="user" v-if="route.path === getUserRoute(user)"/>
<UserFollowersPage :user="user" v-if="route.path === getUserRoute(user) + '/followers'"/>
<UserFollowingPage :user="user" v-if="route.path === getUserRoute(user) + '/following'"/>
</div>
<div v-if="!userAccessible">
This account is private.
</div>
</StandardCenteredPage>
<StandardCenteredPage v-if="userNotFound">
<h3>{{ $t('userPage.notFound.title') }}</h3>
<p>{{ $t('userPage.notFound.description') }}</p>
</StandardCenteredPage>
</q-page>
</template>
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import {onMounted, ref, Ref, watch} from 'vue';
import {User, UserRelationship} from 'src/api/main/auth';
import api from 'src/api/main';
import {useRoute} from 'vue-router';
import {useAuthStore} from 'stores/auth-store';
import {useI18n} from 'vue-i18n';
import {useQuasar} from 'quasar';
import {showApiErrorToast} from 'src/utils';
import PageMenu from 'components/PageMenu.vue';
import UserSubmissionsPage from 'pages/user/UserSubmissionsPage.vue';
import {getUserRoute} from 'src/router/user-routing';
import UserFollowersPage from 'pages/user/UserFollowersPage.vue';
import UserFollowingPage from 'pages/user/UserFollowingPage.vue';
const route = useRoute();
const authStore = useAuthStore();
const i18n = useI18n();
const quasar = useQuasar();
const user: Ref<User | undefined> = ref();
const relationship: Ref<UserRelationship | undefined> = ref();
const isOwnUser = ref(false);
const userNotFound = ref(false);
const userAccessible = ref(false);
// If the user id changes, we have to manually reload the new user, since we
// will end up on the same route component, which means the router won't
// re-render.
watch(route, async (updatedRoute) => {
const userId = updatedRoute.params.userId[0];
if (!user.value || user.value.id !== userId) {
await loadUser(userId);
}
});
onMounted(async () => {
const userId = route.params.userId[0] as string;
await loadUser(userId);
});
async function loadUser(id: string) {
try {
user.value = await api.auth.getUser(id, authStore);
isOwnUser.value = authStore.loggedIn && user.value.id === authStore.user?.id;
userAccessible.value = await api.auth.isUserAccessible(id, authStore);
await loadRelationship();
} catch (error: any) {
user.value = undefined;
relationship.value = undefined;
isOwnUser.value = false;
userAccessible.value = false;
if (error.response && error.response.status === 404) {
userNotFound.value = true;
} else {
showApiErrorToast(i18n, quasar);
}
}
}
async function loadRelationship() {
if (authStore.user && user.value && userAccessible.value && authStore.user.id !== user.value.id) {
try {
relationship.value = await api.auth.getRelationshipTo(authStore.user.id, user.value.id, authStore);
} catch (error) {
relationship.value = undefined;
showApiErrorToast(i18n, quasar);
}
} else {
relationship.value = undefined;
}
}
async function followUser() {
if (user.value) {
try {
await api.auth.followUser(user.value?.id, authStore);
await loadRelationship();
} catch (error) {
showApiErrorToast(i18n, quasar);
}
}
}
async function unfollowUser() {
if (user.value) {
try {
await api.auth.unfollowUser(user.value?.id, authStore);
await loadRelationship();
} catch (error) {
showApiErrorToast(i18n, quasar);
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,48 @@
<template>
<div>
<div v-if="recentSubmissions.length > 0">
<q-list separator>
<ExerciseSubmissionListItem
v-for="sub in recentSubmissions"
:submission="sub"
:key="sub.id"
:show-name="false"
/>
</q-list>
</div>
</div>
</template>
<script setup lang="ts">
import {useI18n} from 'vue-i18n';
import {useQuasar} from 'quasar';
import {useAuthStore} from 'stores/auth-store';
import {onMounted, ref, Ref} from 'vue';
import {ExerciseSubmission} from 'src/api/main/submission';
import {User} from 'src/api/main/auth';
import api from 'src/api/main';
import ExerciseSubmissionListItem from 'components/ExerciseSubmissionListItem.vue';
import {showApiErrorToast} from 'src/utils';
interface Props {
user: User;
}
const props = defineProps<Props>();
const i18n = useI18n();
const quasar = useQuasar();
const authStore = useAuthStore();
const recentSubmissions: Ref<ExerciseSubmission[]> = ref([]);
onMounted(async () => {
try {
recentSubmissions.value = await api.users.getRecentSubmissions(props.user.id, authStore);
} catch (error: any) {
showApiErrorToast(i18n, quasar);
}
});
</script>
<style scoped>
</style>

View File

@ -10,7 +10,7 @@ import RegisterPage from 'pages/auth/RegisterPage.vue';
import RegistrationSuccessPage from 'pages/auth/RegistrationSuccessPage.vue';
import ActivationPage from 'pages/auth/ActivationPage.vue';
import SubmissionPage from 'pages/SubmissionPage.vue';
import UserPage from 'pages/UserPage.vue';
import UserPage from 'pages/user/UserPage.vue';
import UserSettingsPage from 'pages/auth/UserSettingsPage.vue';
import UserSearchPage from 'pages/UserSearchPage.vue';
@ -28,15 +28,11 @@ const routes: RouteRecordRaw[] = [
children: [
{ path: '', component: GymSearchPage },
{ path: 'users', component: UserSearchPage },
{
path: 'users/:userId',
children: [
{ path: '', component: UserPage },
{ path: 'settings', component: UserSettingsPage }
]
{ path: 'users/:userId/settings', component: UserSettingsPage },
{ // Match anything under /users/:userId to the UserPage, since it manages sub-pages manually.
path: 'users/:userId+',
component: UserPage
},
// { path: 'users/:userId', component: UserPage },
// { path: 'users/:userId/settings', component: UserSettingsPage },
{
path: 'gyms/:countryCode/:cityShortName/:gymShortName',
component: GymPage,