Added lots of stuff!!!
This commit is contained in:
parent
97c69ea34d
commit
ae3c22422c
|
@ -49,6 +49,7 @@ public class SecurityConfig {
|
||||||
"/submissions/**",
|
"/submissions/**",
|
||||||
"/auth/reset-password",
|
"/auth/reset-password",
|
||||||
"/auth/users/*",
|
"/auth/users/*",
|
||||||
|
"/auth/users/*/access",
|
||||||
"/auth/users/*/followers",
|
"/auth/users/*/followers",
|
||||||
"/auth/users/*/following",
|
"/auth/users/*/following",
|
||||||
"/users/*/recent-submissions"
|
"/users/*/recent-submissions"
|
||||||
|
|
|
@ -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.dto.SubmissionResponse;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(path = "/submissions")
|
@RequestMapping(path = "/submissions")
|
||||||
|
@ -20,4 +20,10 @@ public class SubmissionController {
|
||||||
public SubmissionResponse getSubmission(@PathVariable String submissionId) {
|
public SubmissionResponse getSubmission(@PathVariable String submissionId) {
|
||||||
return submissionService.getSubmission(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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,4 +132,15 @@ public class ExerciseSubmissionService {
|
||||||
}
|
}
|
||||||
return response;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.dto.*;
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
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 nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
@ -12,9 +13,11 @@ import org.springframework.web.bind.annotation.*;
|
||||||
@RestController
|
@RestController
|
||||||
public class UserController {
|
public class UserController {
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final UserAccessService userAccessService;
|
||||||
|
|
||||||
public UserController(UserService userService) {
|
public UserController(UserService userService, UserAccessService userAccessService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.userAccessService = userAccessService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,6 +36,11 @@ public class UserController {
|
||||||
return userService.getUser(userId);
|
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.
|
* Endpoint for updating one's own password.
|
||||||
* @param user The user that's updating their password.
|
* @param user The user that's updating their password.
|
||||||
|
@ -71,6 +79,11 @@ public class UserController {
|
||||||
return userService.updatePreferences(user.getId(), payload);
|
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")
|
@PostMapping(path = "/auth/users/{userId}/followers")
|
||||||
public ResponseEntity<Void> followUser(@AuthenticationPrincipal User myUser, @PathVariable String userId) {
|
public ResponseEntity<Void> followUser(@AuthenticationPrincipal User myUser, @PathVariable String userId) {
|
||||||
userService.followUser(myUser.getId(), userId);
|
userService.followUser(myUser.getId(), userId);
|
||||||
|
@ -92,4 +105,14 @@ public class UserController {
|
||||||
public Page<UserResponse> getFollowing(@AuthenticationPrincipal User user, Pageable pageable) {
|
public Page<UserResponse> getFollowing(@AuthenticationPrincipal User user, Pageable pageable) {
|
||||||
return userService.getFollowing(user.getId(), 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||||
|
|
||||||
|
public record UserAccessResponse(boolean accessible) {}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||||
|
|
||||||
|
public record UserRelationshipResponse(
|
||||||
|
boolean following,
|
||||||
|
boolean followedBy
|
||||||
|
) {}
|
|
@ -36,6 +36,9 @@ public class UserAccessService {
|
||||||
if (targetUser != null && !targetUser.getPreferences().isAccountPrivate()) {
|
if (targetUser != null && !targetUser.getPreferences().isAccountPrivate()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (user != null && targetUser != null && user.getId().equals(targetUser.getId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return user != null && followingRepository.existsByFollowedUserAndFollowingUser(targetUser, user);
|
return user != null && followingRepository.existsByFollowedUserAndFollowingUser(targetUser, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +82,10 @@ public class UserAccessService {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public boolean currentUserHasAccess(String userId) {
|
public boolean currentUserHasAccess(String userId) {
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
User user = null;
|
||||||
if (auth instanceof TokenAuthentication tokenAuth) {
|
if (auth instanceof TokenAuthentication tokenAuth) {
|
||||||
return userHasAccess(tokenAuth.user(), userId);
|
user = tokenAuth.user();
|
||||||
}
|
}
|
||||||
return false;
|
return userHasAccess(user, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ public class UserService {
|
||||||
private final UserActivationCodeRepository activationCodeRepository;
|
private final UserActivationCodeRepository activationCodeRepository;
|
||||||
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
||||||
private final UserFollowingRepository userFollowingRepository;
|
private final UserFollowingRepository userFollowingRepository;
|
||||||
|
private final UserAccessService userAccessService;
|
||||||
private final ULID ulid;
|
private final ULID ulid;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JavaMailSender mailSender;
|
private final JavaMailSender mailSender;
|
||||||
|
@ -51,7 +52,7 @@ public class UserService {
|
||||||
UserPreferencesRepository userPreferencesRepository,
|
UserPreferencesRepository userPreferencesRepository,
|
||||||
UserActivationCodeRepository activationCodeRepository,
|
UserActivationCodeRepository activationCodeRepository,
|
||||||
PasswordResetCodeRepository passwordResetCodeRepository,
|
PasswordResetCodeRepository passwordResetCodeRepository,
|
||||||
UserFollowingRepository userFollowingRepository, ULID ulid,
|
UserFollowingRepository userFollowingRepository, UserAccessService userAccessService, ULID ulid,
|
||||||
PasswordEncoder passwordEncoder,
|
PasswordEncoder passwordEncoder,
|
||||||
JavaMailSender mailSender
|
JavaMailSender mailSender
|
||||||
) {
|
) {
|
||||||
|
@ -61,6 +62,7 @@ public class UserService {
|
||||||
this.activationCodeRepository = activationCodeRepository;
|
this.activationCodeRepository = activationCodeRepository;
|
||||||
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
||||||
this.userFollowingRepository = userFollowingRepository;
|
this.userFollowingRepository = userFollowingRepository;
|
||||||
|
this.userAccessService = userAccessService;
|
||||||
this.ulid = ulid;
|
this.ulid = ulid;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.mailSender = mailSender;
|
this.mailSender = mailSender;
|
||||||
|
@ -280,6 +282,7 @@ public class UserService {
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void followUser(String followerId, String followedId) {
|
public void followUser(String followerId, String followedId) {
|
||||||
|
if (followerId.equals(followedId)) return;
|
||||||
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
User followed = userRepository.findById(followedId).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
|
@Transactional
|
||||||
public void unfollowUser(String followerId, String followedId) {
|
public void unfollowUser(String followerId, String followedId) {
|
||||||
|
if (followerId.equals(followedId)) return;
|
||||||
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
User followed = userRepository.findById(followedId).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) {
|
public Page<UserResponse> getFollowers(String userId, Pageable pageable) {
|
||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
userAccessService.enforceUserAccess(user);
|
||||||
return userFollowingRepository.findAllByFollowedUserOrderByCreatedAtDesc(user, pageable)
|
return userFollowingRepository.findAllByFollowedUserOrderByCreatedAtDesc(user, pageable)
|
||||||
.map(UserFollowing::getFollowingUser)
|
.map(UserFollowing::getFollowingUser)
|
||||||
.map(UserResponse::new);
|
.map(UserResponse::new);
|
||||||
|
@ -309,8 +314,24 @@ public class UserService {
|
||||||
public Page<UserResponse> getFollowing(String userId, Pageable pageable) {
|
public Page<UserResponse> getFollowing(String userId, Pageable pageable) {
|
||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
userAccessService.enforceUserAccess(user);
|
||||||
return userFollowingRepository.findAllByFollowingUserOrderByCreatedAtDesc(user, pageable)
|
return userFollowingRepository.findAllByFollowingUserOrderByCreatedAtDesc(user, pageable)
|
||||||
.map(UserFollowing::getFollowedUser)
|
.map(UserFollowing::getFollowedUser)
|
||||||
.map(UserResponse::new);
|
.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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,3 +17,5 @@ spring.mail.properties.mail.smtp.timeout=10000
|
||||||
app.auth.private-key-location=./private_key.der
|
app.auth.private-key-location=./private_key.der
|
||||||
app.web-origin=http://localhost:9000
|
app.web-origin=http://localhost:9000
|
||||||
app.cdn-origin=http://localhost:8082
|
app.cdn-origin=http://localhost:8082
|
||||||
|
|
||||||
|
#logging.level.root=DEBUG
|
||||||
|
|
|
@ -27,6 +27,11 @@ export interface UserPersonalDetails {
|
||||||
sex: PersonSex;
|
sex: PersonSex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserRelationship {
|
||||||
|
following: boolean;
|
||||||
|
followedBy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
userId: string;
|
userId: string;
|
||||||
accountPrivate: boolean;
|
accountPrivate: boolean;
|
||||||
|
@ -104,6 +109,11 @@ class AuthModule {
|
||||||
return response.data;
|
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) {
|
public async updatePassword(newPassword: string, authStore: AuthStoreType) {
|
||||||
await api.post(
|
await api.post(
|
||||||
'/auth/me/password',
|
'/auth/me/password',
|
||||||
|
@ -142,6 +152,29 @@ class AuthModule {
|
||||||
const response = await api.post('/auth/me/preferences', newPreferences, authStore.axiosConfig);
|
const response = await api.post('/auth/me/preferences', newPreferences, authStore.axiosConfig);
|
||||||
return response.data;
|
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;
|
export default AuthModule;
|
||||||
|
|
|
@ -66,6 +66,10 @@ class SubmissionsModule {
|
||||||
const response = await api.post(`/gyms/${gymId}/submissions`, payload, authStore.axiosConfig);
|
const response = await api.post(`/gyms/${gymId}/submissions`, payload, authStore.axiosConfig);
|
||||||
return parseSubmission(response.data);
|
return parseSubmission(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteSubmission(id: string, authStore: AuthStoreType) {
|
||||||
|
await api.delete(`/submissions/${id}`, authStore.axiosConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SubmissionsModule;
|
export default SubmissionsModule;
|
||||||
|
|
|
@ -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>
|
|
@ -3,7 +3,7 @@
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ user.name }}</q-item-label>
|
<q-item-label>{{ user.name }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side top>
|
<q-item-section side top v-if="user.submissionCount">
|
||||||
<q-badge color="primary" :label="submissionCountLabel"/>
|
<q-badge color="primary" :label="submissionCountLabel"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
|
@ -20,6 +20,13 @@
|
||||||
<p>
|
<p>
|
||||||
{{ submission.performedAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
|
{{ submission.performedAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
|
||||||
</p>
|
</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>
|
</StandardCenteredPage>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,11 +40,18 @@ import { useRoute, useRouter } from 'vue-router';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { getFileUrl } from 'src/api/cdn';
|
import { getFileUrl } from 'src/api/cdn';
|
||||||
import { getGymRoute } from 'src/router/gym-routing';
|
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 submission: Ref<ExerciseSubmission | undefined> = ref();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const submissionId = route.params.submissionId as string;
|
const submissionId = route.params.submissionId as string;
|
||||||
|
@ -48,6 +62,18 @@ onMounted(async () => {
|
||||||
await router.push('/');
|
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>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.submission-video {
|
.submission-video {
|
||||||
|
|
|
@ -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>
|
|
|
@ -2,23 +2,14 @@
|
||||||
<q-page>
|
<q-page>
|
||||||
<StandardCenteredPage v-if="gym">
|
<StandardCenteredPage v-if="gym">
|
||||||
<h3 class="q-my-md text-center">{{ gym.displayName }}</h3>
|
<h3 class="q-my-md text-center">{{ gym.displayName }}</h3>
|
||||||
<q-btn-group spread square push>
|
<PageMenu
|
||||||
<q-btn
|
:base-route="getGymRoute(gym)"
|
||||||
:label="$t('gymPage.home')"
|
:items="[
|
||||||
:to="getGymRoute(gym)"
|
{label: t('gymPage.home')},
|
||||||
:color="homePageSelected ? 'primary' : 'secondary'"
|
{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 />
|
<router-view />
|
||||||
</StandardCenteredPage>
|
</StandardCenteredPage>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
@ -30,9 +21,12 @@ import { useRoute, useRouter } from 'vue-router';
|
||||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||||
import { getGymFromRoute, getGymRoute } from 'src/router/gym-routing';
|
import { getGymFromRoute, getGymRoute } from 'src/router/gym-routing';
|
||||||
import { Gym } from 'src/api/main/gyms';
|
import { Gym } from 'src/api/main/gyms';
|
||||||
|
import PageMenu from "components/PageMenu.vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useI18n().t;
|
||||||
|
|
||||||
const gym: Ref<Gym | undefined> = ref<Gym>();
|
const gym: Ref<Gym | undefined> = ref<Gym>();
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -10,7 +10,7 @@ import RegisterPage from 'pages/auth/RegisterPage.vue';
|
||||||
import RegistrationSuccessPage from 'pages/auth/RegistrationSuccessPage.vue';
|
import RegistrationSuccessPage from 'pages/auth/RegistrationSuccessPage.vue';
|
||||||
import ActivationPage from 'pages/auth/ActivationPage.vue';
|
import ActivationPage from 'pages/auth/ActivationPage.vue';
|
||||||
import SubmissionPage from 'pages/SubmissionPage.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 UserSettingsPage from 'pages/auth/UserSettingsPage.vue';
|
||||||
import UserSearchPage from 'pages/UserSearchPage.vue';
|
import UserSearchPage from 'pages/UserSearchPage.vue';
|
||||||
|
|
||||||
|
@ -28,15 +28,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
children: [
|
children: [
|
||||||
{ path: '', component: GymSearchPage },
|
{ path: '', component: GymSearchPage },
|
||||||
{ path: 'users', component: UserSearchPage },
|
{ path: 'users', component: UserSearchPage },
|
||||||
{
|
{ path: 'users/:userId/settings', component: UserSettingsPage },
|
||||||
path: 'users/:userId',
|
{ // Match anything under /users/:userId to the UserPage, since it manages sub-pages manually.
|
||||||
children: [
|
path: 'users/:userId+',
|
||||||
{ path: '', component: UserPage },
|
component: UserPage
|
||||||
{ path: 'settings', component: UserSettingsPage }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
// { path: 'users/:userId', component: UserPage },
|
|
||||||
// { path: 'users/:userId/settings', component: UserSettingsPage },
|
|
||||||
{
|
{
|
||||||
path: 'gyms/:countryCode/:cityShortName/:gymShortName',
|
path: 'gyms/:countryCode/:cityShortName/:gymShortName',
|
||||||
component: GymPage,
|
component: GymPage,
|
||||||
|
|
Loading…
Reference in New Issue