Cleaned up UserService.java, refactored user page to use profile information instead of other endpoints.
This commit is contained in:
parent
346c5d9813
commit
bb5cf53908
|
@ -50,6 +50,7 @@ public class SecurityConfig {
|
|||
"/submissions/**",
|
||||
"/auth/reset-password",
|
||||
"/auth/users/*",
|
||||
"/auth/users/*/profile",
|
||||
"/auth/users/*/access",
|
||||
"/auth/users/*/followers",
|
||||
"/auth/users/*/following",
|
||||
|
|
|
@ -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.model.UserPreferences;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserAccessService;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
@ -36,6 +37,19 @@ public class UserController {
|
|||
return userService.getUser(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the information that the app will typically need to display a
|
||||
* user's profile page. Some information may be omitted if the user has
|
||||
* set their {@link UserPreferences#isAccountPrivate()}
|
||||
* to true, and the requesting user isn't following them.
|
||||
* @param userId The id of the user to fetch profile information for.
|
||||
* @return The user's profile information.
|
||||
*/
|
||||
@GetMapping(path = "/auth/users/{userId}/profile")
|
||||
public UserProfileResponse getUserProfile(@PathVariable String userId) {
|
||||
return userService.getProfile(userId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/auth/users/{userId}/access")
|
||||
public UserAccessResponse getUserAccess(@PathVariable String userId) {
|
||||
return new UserAccessResponse(userAccessService.currentUserHasAccess(userId));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserFollowRequest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
|
@ -11,4 +12,6 @@ import java.time.LocalDateTime;
|
|||
public interface UserFollowRequestRepository extends JpaRepository<UserFollowRequest, Long> {
|
||||
@Modifying
|
||||
void deleteAllByCreatedAtBefore(LocalDateTime cutoff);
|
||||
|
||||
boolean existsByRequestingUserAndUserToFollowAndApprovedIsNull(User requestingUser, User userToFollow);
|
||||
}
|
||||
|
|
|
@ -20,4 +20,6 @@ public interface UserFollowingRepository extends JpaRepository<UserFollowing, Lo
|
|||
|
||||
long countByFollowedUser(User followedUser);
|
||||
long countByFollowingUser(User followingUser);
|
||||
long countByFollowedUserId(String id);
|
||||
long countByFollowingUserId(String id);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
/**
|
||||
* Response that contains all the information needed to show a user's profile
|
||||
* page.
|
||||
*/
|
||||
public record UserProfileResponse(
|
||||
String id,
|
||||
String name,
|
||||
String followerCount,
|
||||
String followingCount,
|
||||
boolean followingThisUser,
|
||||
boolean accountPrivate,
|
||||
boolean canAccessThisUser
|
||||
) {}
|
|
@ -0,0 +1,48 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.service;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.EmailResetCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.PasswordResetCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserActivationCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserFollowRequestRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.EmailResetCode;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.PasswordResetCode;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserFollowRequest;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class CleanupService {
|
||||
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
||||
private final UserActivationCodeRepository activationCodeRepository;
|
||||
private final UserFollowRequestRepository followRequestRepository;
|
||||
private final EmailResetCodeRepository emailResetCodeRepository;
|
||||
|
||||
public CleanupService(PasswordResetCodeRepository passwordResetCodeRepository, UserActivationCodeRepository activationCodeRepository, UserFollowRequestRepository followRequestRepository, EmailResetCodeRepository emailResetCodeRepository) {
|
||||
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
||||
this.activationCodeRepository = activationCodeRepository;
|
||||
this.followRequestRepository = followRequestRepository;
|
||||
this.emailResetCodeRepository = emailResetCodeRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled task that periodically removes all old authentication entities
|
||||
* so that they don't clutter up the system.
|
||||
*/
|
||||
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.HOURS)
|
||||
@Transactional
|
||||
public void removeOldAuthEntities() {
|
||||
LocalDateTime passwordResetCodeCutoff = LocalDateTime.now().minus(PasswordResetCode.VALID_FOR);
|
||||
passwordResetCodeRepository.deleteAllByCreatedAtBefore(passwordResetCodeCutoff);
|
||||
LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
||||
activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff);
|
||||
LocalDateTime followRequestCutoff = LocalDateTime.now().minus(UserFollowRequest.VALID_FOR);
|
||||
followRequestRepository.deleteAllByCreatedAtBefore(followRequestCutoff);
|
||||
LocalDateTime emailResetCodeCutoff = LocalDateTime.now().minus(EmailResetCode.VALID_FOR);
|
||||
emailResetCodeRepository.deleteAllByCreatedAtBefore(emailResetCodeCutoff);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ import java.time.LocalDateTime;
|
|||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static nl.andrewlalis.gymboard_api.util.DataUtils.findByIdOrThrow;
|
||||
import static nl.andrewlalis.gymboard_api.util.DataUtils.*;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
@ -283,23 +283,6 @@ public class UserService {
|
|||
emailResetCodeRepository.delete(emailResetCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled task that periodically removes all old authentication entities
|
||||
* so that they don't clutter up the system.
|
||||
*/
|
||||
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.HOURS)
|
||||
@Transactional
|
||||
public void removeOldAuthEntities() {
|
||||
LocalDateTime passwordResetCodeCutoff = LocalDateTime.now().minus(PasswordResetCode.VALID_FOR);
|
||||
passwordResetCodeRepository.deleteAllByCreatedAtBefore(passwordResetCodeCutoff);
|
||||
LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
||||
activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff);
|
||||
LocalDateTime followRequestCutoff = LocalDateTime.now().minus(UserFollowRequest.VALID_FOR);
|
||||
followRequestRepository.deleteAllByCreatedAtBefore(followRequestCutoff);
|
||||
LocalDateTime emailResetCodeCutoff = LocalDateTime.now().minus(EmailResetCode.VALID_FOR);
|
||||
emailResetCodeRepository.deleteAllByCreatedAtBefore(emailResetCodeCutoff);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserPersonalDetailsResponse updatePersonalDetails(String id, UserPersonalDetailsPayload payload) {
|
||||
User user = userRepository.findById(id)
|
||||
|
@ -366,12 +349,14 @@ public class UserService {
|
|||
User followed = findByIdOrThrow(followedId, userRepository);
|
||||
|
||||
if (!userFollowingRepository.existsByFollowedUserAndFollowingUser(followed, follower)) {
|
||||
if (followed.getPreferences().isAccountPrivate()) {
|
||||
if (!followed.getPreferences().isAccountPrivate()) {
|
||||
userFollowingRepository.save(new UserFollowing(followed, follower));
|
||||
return UserFollowResponse.requested();
|
||||
} else {
|
||||
followRequestRepository.save(new UserFollowRequest(follower, followed));
|
||||
return UserFollowResponse.followed();
|
||||
} else {
|
||||
if (!followRequestRepository.existsByRequestingUserAndUserToFollowAndApprovedIsNull(follower, followed)) {
|
||||
followRequestRepository.save(new UserFollowRequest(follower, followed));
|
||||
}
|
||||
return UserFollowResponse.requested();
|
||||
}
|
||||
}
|
||||
return UserFollowResponse.alreadyFollowed();
|
||||
|
@ -382,7 +367,6 @@ public class UserService {
|
|||
if (followerId.equals(followedId)) return;
|
||||
User follower = findByIdOrThrow(followerId, userRepository);
|
||||
User followed = findByIdOrThrow(followedId, userRepository);
|
||||
|
||||
userFollowingRepository.deleteByFollowedUserAndFollowingUser(followed, follower);
|
||||
}
|
||||
|
||||
|
@ -422,12 +406,14 @@ public class UserService {
|
|||
.map(UserResponse::new);
|
||||
}
|
||||
|
||||
public long getFollowerCount(String userId) {
|
||||
return userFollowingRepository.countByFollowedUser(findByIdOrThrow(userId, userRepository));
|
||||
public String getFollowerCount(String userId) {
|
||||
long rawCount = userFollowingRepository.countByFollowedUserId(userId);
|
||||
return formatLargeInt(fuzzInt(rawCount));
|
||||
}
|
||||
|
||||
public long getFollowingCount(String userId) {
|
||||
return userFollowingRepository.countByFollowingUser(findByIdOrThrow(userId, userRepository));
|
||||
public String getFollowingCount(String userId) {
|
||||
long rawCount = userFollowingRepository.countByFollowingUserId(userId);
|
||||
return formatLargeInt(fuzzInt(rawCount));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
@ -457,4 +443,23 @@ public class UserService {
|
|||
payload.description()
|
||||
));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserProfileResponse getProfile(String userId) {
|
||||
User user = findByIdOrThrow(userId, userRepository);
|
||||
boolean canAccessThisUser = userAccessService.currentUserHasAccess(user);
|
||||
boolean followingThisUser = false;
|
||||
if (SecurityContextHolder.getContext().getAuthentication() instanceof TokenAuthentication t) {
|
||||
followingThisUser = userFollowingRepository.existsByFollowedUserAndFollowingUser(user, t.user());
|
||||
}
|
||||
return new UserProfileResponse(
|
||||
user.getId(),
|
||||
user.getName(),
|
||||
getFollowerCount(user.getId()),
|
||||
getFollowingCount(user.getId()),
|
||||
followingThisUser,
|
||||
user.getPreferences().isAccountPrivate(),
|
||||
canAccessThisUser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,4 +17,53 @@ public class DataUtils {
|
|||
public static <T, ID> T findByIdOrThrow(ID id, CrudRepository<T, ID> repo) throws ResponseStatusException {
|
||||
return repo.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a large integer value as a human-readable string with a size
|
||||
* suffix. For example, instead of 1,245,300, we would return "1.2M".
|
||||
* @param value The value to format.
|
||||
* @return The string representation of the value.
|
||||
*/
|
||||
public static String formatLargeInt(long value) {
|
||||
if (value < 1000) return Long.toString(value);
|
||||
long scale = 1000;
|
||||
final long MAX_SCALE = 1_000_000_000;
|
||||
final char[] SCALE_SUFFIXES = {'K', 'M', 'B'};
|
||||
int scaleSuffixIdx = 0;
|
||||
|
||||
while (scale <= MAX_SCALE) {
|
||||
if (value < scale * 1000 || scale == MAX_SCALE) {
|
||||
long baseValue = value / scale;
|
||||
long remainderDigit = (value % scale) / (scale / 10);
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
sb.append(baseValue);
|
||||
if (remainderDigit > 0) sb.append('.').append(remainderDigit);
|
||||
sb.append(SCALE_SUFFIXES[scaleSuffixIdx]);
|
||||
return sb.toString();
|
||||
}
|
||||
scale *= 1000;
|
||||
scaleSuffixIdx++;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
/**
|
||||
* "Fuzzes" an integer value such that it is randomly incremented or
|
||||
* decremented by some amount proportional to its original value, so that
|
||||
* its original true value is obscured slightly.
|
||||
* @param value The value to fuzz.
|
||||
* @return The fuzzed value.
|
||||
*/
|
||||
public static long fuzzInt(long value) {
|
||||
double fuzz;
|
||||
if (value < 1000) {
|
||||
fuzz = 0.01;
|
||||
} else if (value < 1_000_000) {
|
||||
fuzz = 0.0001;
|
||||
} else {
|
||||
fuzz = 0.000001;
|
||||
}
|
||||
double modification = fuzz * Math.random() * value;
|
||||
return value + Math.round(modification);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package nl.andrewlalis.gymboard_api.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static nl.andrewlalis.gymboard_api.util.DataUtils.formatLargeInt;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DataUtilsTest {
|
||||
@Test
|
||||
public void testFormatLargeInt() {
|
||||
assertEquals("0", formatLargeInt(0));
|
||||
assertEquals("1", formatLargeInt(1));
|
||||
assertEquals("42", formatLargeInt(42));
|
||||
assertEquals("999", formatLargeInt(999));
|
||||
assertEquals("1K", formatLargeInt(1000));
|
||||
assertEquals("1K", formatLargeInt(1099));
|
||||
assertEquals("1.1K", formatLargeInt(1100));
|
||||
assertEquals("1.1K", formatLargeInt(1199));
|
||||
assertEquals("25.2K", formatLargeInt(25_231));
|
||||
assertEquals("999K", formatLargeInt(999_000));
|
||||
assertEquals("1M", formatLargeInt(1_000_000));
|
||||
assertEquals("1.5M", formatLargeInt(1_500_000));
|
||||
assertEquals("3B", formatLargeInt(3_000_000_000L));
|
||||
assertEquals("1024B", formatLargeInt(1_024_000_000_000L));
|
||||
}
|
||||
}
|
|
@ -5,4 +5,4 @@
|
|||
|
||||
echo "Starting gymboard-api development server."
|
||||
./gen_keys.d
|
||||
./mvnw spring-boot:run -Dspring-boot.run.profiles=development
|
||||
./mvnw clean spring-boot:run -Dspring-boot.run.profiles=development
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -49,6 +49,22 @@ export interface UserCreationPayload {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
followerCount: string;
|
||||
followingCount: string;
|
||||
followingThisUser: boolean;
|
||||
accountPrivate: boolean;
|
||||
canAccessThisUser: boolean;
|
||||
}
|
||||
|
||||
export enum UserFollowResponse {
|
||||
FOLLOWED = 'FOLLOWED',
|
||||
REQUESTED = 'REQUESTED',
|
||||
ALREADY_FOLLOWED = 'ALREADY_FOLLOWED'
|
||||
}
|
||||
|
||||
class AuthModule {
|
||||
private static readonly TOKEN_REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
|
@ -115,6 +131,11 @@ class AuthModule {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
public async getUserProfile(userId: string, authStore: AuthStoreType): Promise<UserProfile> {
|
||||
const response = await api.get(`/auth/users/${userId}/profile`, authStore.axiosConfig);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async isUserAccessible(
|
||||
userId: string,
|
||||
authStore: AuthStoreType
|
||||
|
@ -245,7 +266,7 @@ class AuthModule {
|
|||
public async followUser(
|
||||
userId: string,
|
||||
authStore: AuthStoreType
|
||||
): Promise<string> {
|
||||
): Promise<UserFollowResponse> {
|
||||
const response = await api.post(
|
||||
`/auth/users/${userId}/followers`,
|
||||
undefined,
|
||||
|
|
|
@ -21,8 +21,8 @@ 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";
|
||||
import PageMenu from 'components/PageMenu.vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
@ -39,14 +39,4 @@ onMounted(async () => {
|
|||
await router.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
const homePageSelected = computed(
|
||||
() => gym.value && getGymRoute(gym.value) === route.fullPath
|
||||
);
|
||||
const submitPageSelected = computed(
|
||||
() => gym.value && route.fullPath === getGymRoute(gym.value) + '/submit'
|
||||
);
|
||||
const leaderboardPageSelected = computed(
|
||||
() => gym.value && route.fullPath === getGymRoute(gym.value) + '/leaderboard'
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -12,20 +12,20 @@
|
|||
</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 {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;
|
||||
userId: string
|
||||
}
|
||||
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);
|
||||
followers.value = await api.auth.getFollowers(props.userId, authStore, 0, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
</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 {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;
|
||||
userId: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const authStore = useAuthStore();
|
||||
|
@ -23,7 +23,7 @@ const following: Ref<User[]> = ref([]);
|
|||
|
||||
onMounted(async () => {
|
||||
following.value = await api.auth.getFollowing(
|
||||
props.user.id,
|
||||
props.userId,
|
||||
authStore,
|
||||
0,
|
||||
10
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
<template>
|
||||
<q-page>
|
||||
<StandardCenteredPage v-if="user">
|
||||
<h3>{{ user?.name }}</h3>
|
||||
<StandardCenteredPage v-if="profile">
|
||||
<h3>{{ profile.name }}</h3>
|
||||
<div v-if="devStore.showDebugInfo">
|
||||
<div>Private: {{ profile.accountPrivate }}</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div>Followers: {{ profile.followerCount }}</div>
|
||||
<div>Following: {{ profile.followingCount }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.loggedIn && !isOwnUser">
|
||||
<q-btn v-if="!profile.followingThisUser" label="Follow" @click="followUser"/>
|
||||
<q-btn v-if="profile.followingThisUser" label="Unfollow" @click="unfollowUser"/>
|
||||
</div>
|
||||
|
||||
<PageMenu
|
||||
:base-route="`/users/${user.id}`"
|
||||
:base-route="`/users/${profile.id}`"
|
||||
:items="[
|
||||
{label: 'Lifts', to: ''},
|
||||
{label: 'Followers', to: 'followers'},
|
||||
|
@ -18,18 +26,21 @@
|
|||
/>
|
||||
|
||||
<!-- 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 v-if="profile.canAccessThisUser">
|
||||
<UserSubmissionsPage :userId="profile.id" v-if="route.path === `/users/${profile.id}`"/>
|
||||
<UserFollowersPage :userId="profile.id" v-if="route.path === `/users/${profile.id}/followers`"/>
|
||||
<UserFollowingPage :userId="profile.id" v-if="route.path === `/users/${profile.id}/following`"/>
|
||||
</div>
|
||||
|
||||
<div v-if="!userAccessible">
|
||||
<!-- If the user can't be accessed, show a placeholder message instead. -->
|
||||
<div v-if="!profile.canAccessThisUser">
|
||||
This account is private.
|
||||
</div>
|
||||
|
||||
</StandardCenteredPage>
|
||||
<StandardCenteredPage v-if="userNotFound">
|
||||
|
||||
<!-- If no user profile was loaded, we show a generic "User not found" message and no content. -->
|
||||
<StandardCenteredPage v-if="!profile">
|
||||
<h3>{{ $t('userPage.notFound.title') }}</h3>
|
||||
<p>{{ $t('userPage.notFound.description') }}</p>
|
||||
</StandardCenteredPage>
|
||||
|
@ -39,36 +50,34 @@
|
|||
<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 {UserFollowResponse, UserProfile} 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 {showApiErrorToast, showInfoToast} 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';
|
||||
import {useDevStore} from 'stores/dev-store';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const devStore = useDevStore();
|
||||
const i18n = useI18n();
|
||||
const quasar = useQuasar();
|
||||
|
||||
const user: Ref<User | undefined> = ref();
|
||||
const relationship: Ref<UserRelationship | undefined> = ref();
|
||||
const profile: Ref<UserProfile | 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) {
|
||||
if (!profile.value || (profile.value.id !== userId)) {
|
||||
await loadUser(userId);
|
||||
}
|
||||
});
|
||||
|
@ -80,41 +89,24 @@ onMounted(async () => {
|
|||
|
||||
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();
|
||||
profile.value = await api.auth.getUserProfile(id, authStore);
|
||||
isOwnUser.value = authStore.loggedIn && profile.value.id === authStore.user?.id;
|
||||
} 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 {
|
||||
if (!(error.response && error.response.status === 404)) {
|
||||
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) {
|
||||
if (profile.value && !profile.value.followingThisUser) {
|
||||
try {
|
||||
await api.auth.followUser(user.value?.id, authStore);
|
||||
await loadRelationship();
|
||||
const result = await api.auth.followUser(profile.value.id, authStore);
|
||||
if (result === UserFollowResponse.FOLLOWED) {
|
||||
await loadUser(profile.value.id);
|
||||
} else if (result === UserFollowResponse.REQUESTED) {
|
||||
showInfoToast(quasar, 'Requested to follow this user!');
|
||||
}
|
||||
} catch (error) {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
|
@ -122,10 +114,10 @@ async function followUser() {
|
|||
}
|
||||
|
||||
async function unfollowUser() {
|
||||
if (user.value) {
|
||||
if (profile.value && profile.value.followingThisUser) {
|
||||
try {
|
||||
await api.auth.unfollowUser(user.value?.id, authStore);
|
||||
await loadRelationship();
|
||||
await api.auth.unfollowUser(profile.value.id, authStore);
|
||||
await loadUser(profile.value.id);
|
||||
} catch (error) {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
|
|
|
@ -19,13 +19,12 @@ 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;
|
||||
userId: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
|
@ -36,7 +35,7 @@ const authStore = useAuthStore();
|
|||
const recentSubmissions: Ref<ExerciseSubmission[]> = ref([]);
|
||||
onMounted(async () => {
|
||||
try {
|
||||
recentSubmissions.value = await api.users.getRecentSubmissions(props.user.id, authStore);
|
||||
recentSubmissions.value = await api.users.getRecentSubmissions(props.userId, authStore);
|
||||
} catch (error: any) {
|
||||
showApiErrorToast(i18n, quasar);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* This store keeps track of global state that's only relevant for development
|
||||
* purposes, like debug flags and settings.
|
||||
*/
|
||||
import {defineStore} from 'pinia';
|
||||
|
||||
interface DevState {
|
||||
showDebugInfo: boolean;
|
||||
}
|
||||
|
||||
export const useDevStore = defineStore('devStore', {
|
||||
state: (): DevState => {
|
||||
return { showDebugInfo: !!process.env.DEV };
|
||||
}
|
||||
});
|
||||
|
||||
export type DevStoreType = ReturnType<typeof useDevStore>;
|
|
@ -1,4 +1,4 @@
|
|||
import {QVueGlobals} from "quasar";
|
||||
import {QVueGlobals} from 'quasar';
|
||||
|
||||
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
|
@ -9,3 +9,11 @@ export function showApiErrorToast(i18n: any, quasar: QVueGlobals) {
|
|||
position: 'top'
|
||||
});
|
||||
}
|
||||
|
||||
export function showInfoToast(quasar: QVueGlobals, translatedMessage: string) {
|
||||
quasar.notify({
|
||||
message: translatedMessage,
|
||||
type: 'info',
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue