Added endpoint for reporting users.
This commit is contained in:
parent
3653fe697e
commit
346c5d9813
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||||
|
|
||||||
|
public record UserReportPayload(
|
||||||
|
String reason,
|
||||||
|
String description
|
||||||
|
) {}
|
|
@ -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()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
Loading…
Reference in New Issue