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) {
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.MimeMessageHelper;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -43,6 +44,7 @@ public class UserService {
private final UserFollowingRepository userFollowingRepository;
private final UserFollowRequestRepository followRequestRepository;
private final UserAccessService userAccessService;
private final UserReportRepository userReportRepository;
private final ULID ulid;
private final PasswordEncoder passwordEncoder;
private final JavaMailSender mailSender;
@ -59,7 +61,7 @@ public class UserService {
EmailResetCodeRepository emailResetCodeRepository, UserFollowingRepository userFollowingRepository,
UserFollowRequestRepository followRequestRepository,
UserAccessService userAccessService,
ULID ulid,
UserReportRepository userReportRepository, ULID ulid,
PasswordEncoder passwordEncoder,
JavaMailSender mailSender
) {
@ -72,6 +74,7 @@ public class UserService {
this.userFollowingRepository = userFollowingRepository;
this.followRequestRepository = followRequestRepository;
this.userAccessService = userAccessService;
this.userReportRepository = userReportRepository;
this.ulid = ulid;
this.passwordEncoder = passwordEncoder;
this.mailSender = mailSender;
@ -439,4 +442,19 @@ public class UserService {
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 {AuthStoreType} from 'stores/auth-store';
import { api } from 'src/api/main/index';
import { AuthStoreType } from 'stores/auth-store';
import Timeout = NodeJS.Timeout;
import {WeightUnit} from 'src/api/main/submission';
import { WeightUnit } from 'src/api/main/submission';
export interface User {
id: string;
@ -15,7 +15,7 @@ export interface User {
export enum PersonSex {
MALE = 'MALE',
FEMALE = 'FEMALE',
UNKNOWN = 'UNKNOWN'
UNKNOWN = 'UNKNOWN',
}
export interface UserPersonalDetails {
@ -104,13 +104,25 @@ class AuthModule {
return response.data;
}
public async getUser(userId: string, authStore: AuthStoreType): Promise<User> {
const response = await api.get(`/auth/users/${userId}`, authStore.axiosConfig);
public async getUser(
userId: string,
authStore: AuthStoreType
): Promise<User> {
const response = await api.get(
`/auth/users/${userId}`,
authStore.axiosConfig
);
return response.data;
}
public async isUserAccessible(userId: string, authStore: AuthStoreType): Promise<boolean> {
const response = await api.get(`/auth/users/${userId}/access`, authStore.axiosConfig);
public async isUserAccessible(
userId: string,
authStore: AuthStoreType
): Promise<boolean> {
const response = await api.get(
`/auth/users/${userId}/access`,
authStore.axiosConfig
);
return response.data.accessible;
}
@ -133,48 +145,131 @@ class AuthModule {
});
}
public async getMyPersonalDetails(authStore: AuthStoreType): Promise<UserPersonalDetails> {
const response = await api.get('/auth/me/personal-details', authStore.axiosConfig);
public async generateEmailResetCode(
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;
}
public async updateMyPersonalDetails(authStore: AuthStoreType, newPersonalDetails: UserPersonalDetails): Promise<UserPersonalDetails> {
const response = await api.post('/auth/me/personal-details', newPersonalDetails, authStore.axiosConfig);
public async updateMyPersonalDetails(
authStore: AuthStoreType,
newPersonalDetails: UserPersonalDetails
): Promise<UserPersonalDetails> {
const response = await api.post(
'/auth/me/personal-details',
newPersonalDetails,
authStore.axiosConfig
);
return response.data;
}
public async getMyPreferences(authStore: AuthStoreType): Promise<UserPreferences> {
const response = await api.get('/auth/me/preferences', authStore.axiosConfig);
public async getMyPreferences(
authStore: AuthStoreType
): Promise<UserPreferences> {
const response = await api.get(
'/auth/me/preferences',
authStore.axiosConfig
);
return response.data;
}
public async updateMyPreferences(authStore: AuthStoreType, newPreferences: UserPreferences): Promise<UserPreferences> {
const response = await api.post('/auth/me/preferences', newPreferences, authStore.axiosConfig);
public async updateMyPreferences(
authStore: AuthStoreType,
newPreferences: UserPreferences
): Promise<UserPreferences> {
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);
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);
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);
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 followUser(
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) {
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;

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. -->
<q-page v-if="!authStore.loggedIn">
<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>
</q-page>
</template>
<script setup lang="ts">
import {onMounted, ref, Ref} from 'vue';
import {getGymFromRoute} from 'src/router/gym-routing';
import { onMounted, ref, Ref } from 'vue';
import { getGymFromRoute } from 'src/router/gym-routing';
import SlimForm from 'components/SlimForm.vue';
import api from 'src/api/main';
import {Gym} from 'src/api/main/gyms';
import {Exercise} from 'src/api/main/exercises';
import {useRoute, useRouter} from 'vue-router';
import {showApiErrorToast, sleep} from 'src/utils';
import {uploadVideoToCDN, VideoProcessingStatus, waitUntilVideoProcessingComplete} from 'src/api/cdn';
import {useAuthStore} from 'stores/auth-store';
import {useI18n} from 'vue-i18n';
import {useQuasar} from "quasar";
import { Gym } from 'src/api/main/gyms';
import { Exercise } from 'src/api/main/exercises';
import { useRoute, useRouter } from 'vue-router';
import { showApiErrorToast, sleep } from 'src/utils';
import {
uploadVideoToCDN,
VideoProcessingStatus,
waitUntilVideoProcessingComplete,
} from 'src/api/cdn';
import { useAuthStore } from 'stores/auth-store';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
const authStore = useAuthStore();
const router = useRouter();
@ -167,16 +173,24 @@ async function onSubmitted() {
// 1. Upload the video to the CDN.
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitUploading');
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.
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitVideoProcessing');
const processingStatus = await waitUntilVideoProcessingComplete(submissionModel.value.videoFileId);
submitButtonLabel.value = i18n.t(
'gymPage.submitPage.submitVideoProcessing'
);
const processingStatus = await waitUntilVideoProcessingComplete(
submissionModel.value.videoFileId
);
// 3. If successful upload, create the submission.
if (processingStatus === VideoProcessingStatus.COMPLETED) {
try {
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitCreatingSubmission');
submitButtonLabel.value = i18n.t(
'gymPage.submitPage.submitCreatingSubmission'
);
await sleep(1000);
const submission = await api.gyms.submissions.createSubmission(
gym.value,
@ -191,7 +205,7 @@ async function onSubmitted() {
quasar.notify({
message: error.response.data.message,
type: 'warning',
position: 'top'
position: 'top',
});
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
await sleep(3000);
@ -199,7 +213,7 @@ async function onSubmitted() {
showApiErrorToast(i18n, quasar);
}
}
// Otherwise, report the failed submission and give up.
// Otherwise, report the failed submission and give up.
} else {
submitButtonLabel.value = i18n.t('gymPage.submitPage.submitFailed');
await sleep(3000);

View File

@ -1,9 +1,6 @@
<template>
<q-list separator>
<q-item
v-for="user in following" :key="user.id"
:to="`/users/${user.id}`"
>
<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>
@ -12,9 +9,9 @@
</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 {
@ -25,10 +22,13 @@ const authStore = useAuthStore();
const following: Ref<User[]> = ref([]);
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>
<style scoped>
</style>
<style scoped></style>