Added endpoint for reporting users.

This commit is contained in:
Andrew Lalis 2023-03-24 16:54:26 +01:00
parent 3653fe697e
commit 346c5d9813
7 changed files with 201 additions and 53 deletions

View File

@ -136,4 +136,10 @@ public class UserController {
public Page<UserResponse> getUserFollowing(@PathVariable String userId, Pageable pageable) { public Page<UserResponse> getUserFollowing(@PathVariable String userId, Pageable pageable) {
return userService.getFollowing(userId, pageable); return userService.getFollowing(userId, pageable);
} }
@PostMapping(path = "/auth/users/{userId}/reports")
public ResponseEntity<Void> reportUser(@PathVariable String userId, @RequestBody UserReportPayload payload) {
userService.reportUser(userId, payload);
return ResponseEntity.ok().build();
}
} }

View File

@ -0,0 +1,9 @@
package nl.andrewlalis.gymboard_api.domains.auth.dao;
import nl.andrewlalis.gymboard_api.domains.auth.model.UserReport;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserReportRepository extends JpaRepository<UserReport, Long> {
}

View File

@ -0,0 +1,6 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
public record UserReportPayload(
String reason,
String description
) {}

View File

@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -43,6 +44,7 @@ public class UserService {
private final UserFollowingRepository userFollowingRepository; private final UserFollowingRepository userFollowingRepository;
private final UserFollowRequestRepository followRequestRepository; private final UserFollowRequestRepository followRequestRepository;
private final UserAccessService userAccessService; private final UserAccessService userAccessService;
private final UserReportRepository userReportRepository;
private final ULID ulid; private final ULID ulid;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final JavaMailSender mailSender; private final JavaMailSender mailSender;
@ -59,7 +61,7 @@ public class UserService {
EmailResetCodeRepository emailResetCodeRepository, UserFollowingRepository userFollowingRepository, EmailResetCodeRepository emailResetCodeRepository, UserFollowingRepository userFollowingRepository,
UserFollowRequestRepository followRequestRepository, UserFollowRequestRepository followRequestRepository,
UserAccessService userAccessService, UserAccessService userAccessService,
ULID ulid, UserReportRepository userReportRepository, ULID ulid,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
JavaMailSender mailSender JavaMailSender mailSender
) { ) {
@ -72,6 +74,7 @@ public class UserService {
this.userFollowingRepository = userFollowingRepository; this.userFollowingRepository = userFollowingRepository;
this.followRequestRepository = followRequestRepository; this.followRequestRepository = followRequestRepository;
this.userAccessService = userAccessService; this.userAccessService = userAccessService;
this.userReportRepository = userReportRepository;
this.ulid = ulid; this.ulid = ulid;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.mailSender = mailSender; this.mailSender = mailSender;
@ -439,4 +442,19 @@ public class UserService {
user1FollowedByUser2 user1FollowedByUser2
); );
} }
@Transactional
public void reportUser(String userId, UserReportPayload payload) {
User user = findByIdOrThrow(userId, userRepository);
User reporter = null;
if (SecurityContextHolder.getContext().getAuthentication() instanceof TokenAuthentication t) {
reporter = findByIdOrThrow(t.user().getId(), userRepository);
}
userReportRepository.save(new UserReport(
user,
reporter,
payload.reason(),
payload.description()
));
}
} }

View File

@ -1,7 +1,7 @@
import {api} from 'src/api/main/index'; import { api } from 'src/api/main/index';
import {AuthStoreType} from 'stores/auth-store'; import { AuthStoreType } from 'stores/auth-store';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import {WeightUnit} from 'src/api/main/submission'; import { WeightUnit } from 'src/api/main/submission';
export interface User { export interface User {
id: string; id: string;
@ -15,7 +15,7 @@ export interface User {
export enum PersonSex { export enum PersonSex {
MALE = 'MALE', MALE = 'MALE',
FEMALE = 'FEMALE', FEMALE = 'FEMALE',
UNKNOWN = 'UNKNOWN' UNKNOWN = 'UNKNOWN',
} }
export interface UserPersonalDetails { export interface UserPersonalDetails {
@ -104,13 +104,25 @@ class AuthModule {
return response.data; return response.data;
} }
public async getUser(userId: string, authStore: AuthStoreType): Promise<User> { public async getUser(
const response = await api.get(`/auth/users/${userId}`, authStore.axiosConfig); userId: string,
authStore: AuthStoreType
): Promise<User> {
const response = await api.get(
`/auth/users/${userId}`,
authStore.axiosConfig
);
return response.data; return response.data;
} }
public async isUserAccessible(userId: string, authStore: AuthStoreType): Promise<boolean> { public async isUserAccessible(
const response = await api.get(`/auth/users/${userId}/access`, authStore.axiosConfig); userId: string,
authStore: AuthStoreType
): Promise<boolean> {
const response = await api.get(
`/auth/users/${userId}/access`,
authStore.axiosConfig
);
return response.data.accessible; return response.data.accessible;
} }
@ -133,48 +145,131 @@ class AuthModule {
}); });
} }
public async getMyPersonalDetails(authStore: AuthStoreType): Promise<UserPersonalDetails> { public async generateEmailResetCode(
const response = await api.get('/auth/me/personal-details', authStore.axiosConfig); newEmail: string,
authStore: AuthStoreType
) {
await api.post(
'/auth/me/email-reset-code',
{ newEmail: newEmail },
authStore.axiosConfig
);
}
public async updateMyEmail(code: string, authStore: AuthStoreType) {
await api.post('/auth/me/email?code=' + code, null, authStore.axiosConfig);
}
public async getMyPersonalDetails(
authStore: AuthStoreType
): Promise<UserPersonalDetails> {
const response = await api.get(
'/auth/me/personal-details',
authStore.axiosConfig
);
return response.data; return response.data;
} }
public async updateMyPersonalDetails(authStore: AuthStoreType, newPersonalDetails: UserPersonalDetails): Promise<UserPersonalDetails> { public async updateMyPersonalDetails(
const response = await api.post('/auth/me/personal-details', newPersonalDetails, authStore.axiosConfig); authStore: AuthStoreType,
newPersonalDetails: UserPersonalDetails
): Promise<UserPersonalDetails> {
const response = await api.post(
'/auth/me/personal-details',
newPersonalDetails,
authStore.axiosConfig
);
return response.data; return response.data;
} }
public async getMyPreferences(authStore: AuthStoreType): Promise<UserPreferences> { public async getMyPreferences(
const response = await api.get('/auth/me/preferences', authStore.axiosConfig); authStore: AuthStoreType
): Promise<UserPreferences> {
const response = await api.get(
'/auth/me/preferences',
authStore.axiosConfig
);
return response.data; return response.data;
} }
public async updateMyPreferences(authStore: AuthStoreType, newPreferences: UserPreferences): Promise<UserPreferences> { public async updateMyPreferences(
const response = await api.post('/auth/me/preferences', newPreferences, authStore.axiosConfig); authStore: AuthStoreType,
newPreferences: UserPreferences
): Promise<UserPreferences> {
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[]> { public async getFollowers(
const response = await api.get(`/auth/users/${userId}/followers?page=${page}&count=${count}`, authStore.axiosConfig); 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; return response.data.content;
} }
public async getFollowing(userId: string, authStore: AuthStoreType, page: number, count: number): Promise<User[]> { public async getFollowing(
const response = await api.get(`/auth/users/${userId}/following?page=${page}&count=${count}`, authStore.axiosConfig); 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; return response.data.content;
} }
public async getRelationshipTo(userId: string, targetUserId: string, authStore: AuthStoreType): Promise<UserRelationship> { public async getRelationshipTo(
const response = await api.get(`/auth/users/${userId}/relationship-to/${targetUserId}`, authStore.axiosConfig); userId: string,
targetUserId: string,
authStore: AuthStoreType
): Promise<UserRelationship> {
const response = await api.get(
`/auth/users/${userId}/relationship-to/${targetUserId}`,
authStore.axiosConfig
);
return response.data; return response.data;
} }
public async followUser(userId: string, authStore: AuthStoreType) { public async followUser(
await api.post(`/auth/users/${userId}/followers`, undefined, authStore.axiosConfig); userId: string,
authStore: AuthStoreType
): Promise<string> {
const response = await api.post(
`/auth/users/${userId}/followers`,
undefined,
authStore.axiosConfig
);
return response.data.result;
} }
public async unfollowUser(userId: string, authStore: AuthStoreType) { public async unfollowUser(userId: string, authStore: AuthStoreType) {
await api.delete(`/auth/users/${userId}/followers`, authStore.axiosConfig); await api.delete(`/auth/users/${userId}/followers`, authStore.axiosConfig);
} }
public async reportUser(
userId: string,
reason: string,
description: string | null,
authStore?: AuthStoreType
) {
await api.post(
`/auth/users/${userId}/reports`,
{ reason, description },
authStore?.axiosConfig
);
}
} }
export default AuthModule; export default AuthModule;

View File

@ -83,24 +83,30 @@ A high-level overview of the submission process is as follows:
<!-- If the user is not logged in, show a link to log in. --> <!-- If the user is not logged in, show a link to log in. -->
<q-page v-if="!authStore.loggedIn"> <q-page v-if="!authStore.loggedIn">
<div class="q-mt-lg text-center"> <div class="q-mt-lg text-center">
<router-link :to="`/login?next=${route.fullPath}`" class="text-primary">Login or register to submit your lift</router-link> <router-link :to="`/login?next=${route.fullPath}`" class="text-primary"
>Login or register to submit your lift</router-link
>
</div> </div>
</q-page> </q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, ref, Ref} from 'vue'; import { onMounted, ref, Ref } from 'vue';
import {getGymFromRoute} from 'src/router/gym-routing'; import { getGymFromRoute } from 'src/router/gym-routing';
import SlimForm from 'components/SlimForm.vue'; import SlimForm from 'components/SlimForm.vue';
import api from 'src/api/main'; import api from 'src/api/main';
import {Gym} from 'src/api/main/gyms'; import { Gym } from 'src/api/main/gyms';
import {Exercise} from 'src/api/main/exercises'; import { Exercise } from 'src/api/main/exercises';
import {useRoute, useRouter} from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import {showApiErrorToast, sleep} from 'src/utils'; import { showApiErrorToast, sleep } from 'src/utils';
import {uploadVideoToCDN, VideoProcessingStatus, waitUntilVideoProcessingComplete} from 'src/api/cdn'; import {
import {useAuthStore} from 'stores/auth-store'; uploadVideoToCDN,
import {useI18n} from 'vue-i18n'; VideoProcessingStatus,
import {useQuasar} from "quasar"; waitUntilVideoProcessingComplete,
} from 'src/api/cdn';
import { useAuthStore } from 'stores/auth-store';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
const authStore = useAuthStore(); const authStore = useAuthStore();
const router = useRouter(); const router = useRouter();
@ -167,16 +173,24 @@ async function onSubmitted() {
// 1. Upload the video to the CDN. // 1. Upload the video to the CDN.
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploading'); submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploading');
await sleep(1000); await sleep(1000);
submissionModel.value.videoFileId = await uploadVideoToCDN(selectedVideoFile.value); submissionModel.value.videoFileId = await uploadVideoToCDN(
selectedVideoFile.value
);
// 2. Wait for the video to be processed. // 2. Wait for the video to be processed.
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitVideoProcessing'); submitButtonLabel.value = i18n.t(
const processingStatus = await waitUntilVideoProcessingComplete(submissionModel.value.videoFileId); 'gymPage.submitPage.submitVideoProcessing'
);
const processingStatus = await waitUntilVideoProcessingComplete(
submissionModel.value.videoFileId
);
// 3. If successful upload, create the submission. // 3. If successful upload, create the submission.
if (processingStatus === VideoProcessingStatus.COMPLETED) { if (processingStatus === VideoProcessingStatus.COMPLETED) {
try { try {
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitCreatingSubmission'); submitButtonLabel.value = i18n.t(
'gymPage.submitPage.submitCreatingSubmission'
);
await sleep(1000); await sleep(1000);
const submission = await api.gyms.submissions.createSubmission( const submission = await api.gyms.submissions.createSubmission(
gym.value, gym.value,
@ -191,7 +205,7 @@ async function onSubmitted() {
quasar.notify({ quasar.notify({
message: error.response.data.message, message: error.response.data.message,
type: 'warning', type: 'warning',
position: 'top' position: 'top',
}); });
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed'); submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
await sleep(3000); await sleep(3000);
@ -199,7 +213,7 @@ async function onSubmitted() {
showApiErrorToast(i18n, quasar); showApiErrorToast(i18n, quasar);
} }
} }
// Otherwise, report the failed submission and give up. // Otherwise, report the failed submission and give up.
} else { } else {
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed'); submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
await sleep(3000); await sleep(3000);

View File

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