Browse Source

adiciona delete

pull/2429/head
yasminddias 6 months ago
parent
commit
4e5d9e5271
25 changed files with 270 additions and 52 deletions
  1. 11
      backend/config/settings/base.py
  2. 9
      backend/users/serializers.py
  3. 5
      backend/users/urls.py
  4. 35
      backend/users/views.py
  5. 16
      frontend/components/layout/TheHeader.vue
  6. 15
      frontend/components/member/FormCreate.vue
  7. 6
      frontend/components/tasks/toolbar/forms/FormComment.vue
  8. 22
      frontend/domain/models/user/user.ts
  9. 3
      frontend/i18n/de/header.js
  10. 11
      frontend/i18n/de/rules.js
  11. 3
      frontend/i18n/de/user.js
  12. 3
      frontend/i18n/en/header.js
  13. 13
      frontend/i18n/en/rules.js
  14. 8
      frontend/i18n/en/user.js
  15. 3
      frontend/i18n/fr/header.js
  16. 11
      frontend/i18n/fr/rules.js
  17. 3
      frontend/i18n/fr/user.js
  18. 3
      frontend/i18n/zh/header.js
  19. 12
      frontend/i18n/zh/rules.js
  20. 5
      frontend/i18n/zh/user.js
  21. 7
      frontend/plugins/services.ts
  22. 10
      frontend/repositories/auth/apiAuthRepository.ts
  23. 71
      frontend/repositories/user/apiUserRepository.ts
  24. 16
      frontend/rules/index.js
  25. 21
      frontend/store/auth.js

11
backend/config/settings/base.py

@ -156,7 +156,8 @@ REST_FRAMEWORK = {
# or allow read-only access for unauthenticated users.
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly",
"rest_framework.permissions.IsAuthenticated",
# "rest_framework.permissions.IsAuthenticated",
"rest_framework.permissions.AllowAny"
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
@ -233,7 +234,13 @@ ALLOWED_HOSTS = ["*"]
if DEBUG:
CORS_ORIGIN_ALLOW_ALL = True
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:3000", "http://0.0.0.0:3000", "http://localhost:3000"]
CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:8000",
"http://localhost:8000",
"http://127.0.0.1:3000",
"http://0.0.0.0:3000",
"http://localhost:3000",
]
CSRF_TRUSTED_ORIGINS += env.list("CSRF_TRUSTED_ORIGINS", [])
# Batch size for importing data

9
backend/users/serializers.py

@ -5,4 +5,11 @@ from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ("id", "username", "is_superuser", "is_staff")
fields = ("id", "username", "email", "is_staff", "is_superuser", "is_active")
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ("id", "username", "email", "is_staff", "is_superuser", "is_active",
"first_name", "last_name", "date_joined")

5
backend/users/urls.py

@ -1,10 +1,11 @@
from django.urls import include, path
from .views import Me, UserCreation, Users
from .views import Me, UserCreation, Users, UserDetail
urlpatterns = [
path(route="me", view=Me.as_view(), name="me"),
path(route="users/<int:user_id>/", view=UserDetail.as_view(), name="user_detail"),
path(route="users", view=Users.as_view(), name="user_list"),
path(route="users/create", view=UserCreation.as_view(), name="user_create"),
path(route="users/create/", view=UserCreation.as_view(), name="user_create"),
path("auth/", include("dj_rest_auth.urls")),
]

35
backend/users/views.py

@ -2,11 +2,10 @@ from dj_rest_auth.registration.serializers import RegisterSerializer
from django.contrib.auth.models import User
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics, status
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.permissions import IsAdminUser, IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer
from .serializers import UserSerializer, UserDetailSerializer
from projects.permissions import IsProjectAdmin
@ -14,22 +13,44 @@ class Me(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
serializer = UserSerializer(request.user, context={"request": request})
serializer = UserDetailSerializer(request.user, context={"request": request})
return Response(serializer.data)
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
serializer_class = UserDetailSerializer
lookup_url_kwarg = "user_id"
permission_classes = [IsAuthenticated & (IsProjectAdmin | IsAdminUser)]
filter_backends = (DjangoFilterBackend,)
filterset_fields = ("username",)
def get_queryset(self):
return self.queryset
def perform_update(self, serializer):
if serializer.validated_data.get("is_superuser") and not self.request.user.is_superuser:
raise PermissionDenied("You do not have permission to make this user a superuser.")
if serializer.validated_data.get("is_staff") and not self.request.user.is_superuser:
raise PermissionDenied("You do not have permission to make this user a staff member.")
return super().perform_update(serializer)
def perform_destroy(self, instance):
if instance == self.request.user:
raise PermissionDenied("You cannot delete your own account.")
instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class Users(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated & IsProjectAdmin]
pagination_class = None
filter_backends = (DjangoFilterBackend, filters.SearchFilter)
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
search_fields = ("username",)
ordering_fields = ("username", "email", "is_staff", "is_superuser")
ordering = ("username",) # Default ordering
# Direction can be controlled by using "-" prefix (e.g., "-username" for descending)
class UserCreation(generics.CreateAPIView):
serializer_class = RegisterSerializer
permission_classes = [IsAuthenticated & IsAdminUser]
permission_classes = [AllowAny]
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)

16
frontend/components/layout/TheHeader.vue

@ -29,6 +29,14 @@
>
{{ $t('header.projects') }}
</v-btn>
<v-btn
v-if="isAuthenticated && isStaff"
text
class="text-capitalize"
@click="$router.push(localePath('/users'))"
>
{{ $t('header.users') }}
</v-btn>
<v-menu v-if="!isAuthenticated" open-on-hover offset-y>
<template #activator="{ on }">
<v-btn text v-on="on">
@ -46,9 +54,13 @@
</v-list-item>
</v-list>
</v-menu>
<v-btn v-if="!isAuthenticated" outlined @click="$router.push(localePath('/auth'))">
<v-btn v-if="!isAuthenticated" outlined class="mr-2" @click="$router.push(localePath('/auth'))">
{{ $t('user.login') }}
</v-btn>
<v-btn v-if="!isAuthenticated" outlined color="success"
@click="$router.push(localePath('/register'))">
{{ $t('user.register') }}
</v-btn>
<v-menu v-if="isAuthenticated" offset-y z-index="200">
<template #activator="{ on }">
<v-btn on icon v-on="on">
@ -114,7 +126,7 @@ export default {
},
computed: {
...mapGetters('auth', ['isAuthenticated', 'getUsername']),
...mapGetters('auth', ['isAuthenticated', 'isStaff', 'getUsername']),
...mapGetters('projects', ['currentProject']),
...mapGetters('config', ['isRTL']),

15
frontend/components/member/FormCreate.vue

@ -58,7 +58,7 @@ import Vue from 'vue'
import BaseCard from '@/components/utils/BaseCard.vue'
import { MemberItem } from '~/domain/models/member/member'
import { RoleItem } from '~/domain/models/role/role'
import { UserItem } from '~/domain/models/user/user'
import { User } from '~/domain/models/user/user'
export default Vue.extend({
components: {
@ -80,11 +80,11 @@ export default Vue.extend({
return {
isLoading: false,
valid: false,
users: [] as UserItem[],
users: [] as User[],
roles: [] as RoleItem[],
username: '',
rules: {
userRequired: (v: UserItem) => (!!v && !!v.username) || 'Required',
userRequired: (v: User) => (!!v && !!v.username) || 'Required',
roleRequired: (v: RoleItem) => (!!v && !!v.name) || 'Required'
},
mdiAccount,
@ -94,18 +94,21 @@ export default Vue.extend({
async fetch() {
this.isLoading = true
this.users = await this.$repositories.user.list(this.username)
const list = await this.$services.user.list(this.username)
this.users = list.results
this.isLoading = false
},
computed: {
user: {
get(): UserItem {
get(): User {
return {
id: this.value.user,
username: this.value.username,
email: '',
isStaff: false,
isSuperuser: false
isSuperuser: false,
isActive: false,
}
},
set(val: MemberItem) {

6
frontend/components/tasks/toolbar/forms/FormComment.vue

@ -24,7 +24,7 @@ import Comment from '@/components/comment/Comment.vue'
import FormCreate from '@/components/comment/FormCreate.vue'
import BaseCard from '@/components/utils/BaseCard.vue'
import { CommentItem } from '~/domain/models/comment/comment'
import { UserItem } from '~/domain/models/user/user'
import { User } from '~/domain/models/user/user'
export default Vue.extend({
components: {
@ -42,7 +42,7 @@ export default Vue.extend({
data() {
return {
user: {} as UserItem,
user: {} as User,
comments: [] as CommentItem[]
}
},
@ -60,7 +60,7 @@ export default Vue.extend({
},
async created() {
this.user = await this.$repositories.user.getProfile()
this.user = await this.$services.user.getProfile()
},
methods: {

22
frontend/domain/models/user/user.ts

@ -1,8 +1,26 @@
export class UserItem {
export class User {
constructor(
readonly id: number,
readonly username: string,
readonly email: string,
readonly isActive: boolean,
readonly isSuperuser: boolean,
readonly isStaff: boolean
readonly isStaff: boolean,
) {}
}
export class UserDetails extends User {
constructor(
readonly id: number,
readonly username: string,
readonly email: string,
readonly firstName: string,
readonly lastName: string,
readonly isActive: boolean,
readonly isSuperuser: boolean,
readonly isStaff: boolean,
readonly dateJoined: string
) {
super(id, username, email, isActive, isSuperuser, isStaff)
}
}

3
frontend/i18n/de/header.js

@ -1,3 +1,4 @@
export default {
projects: 'Projekte'
projects: 'projekte',
users: 'benutzer',
}

11
frontend/i18n/de/rules.js

@ -8,7 +8,8 @@ export default {
},
userNameRules: {
userNameRequired: 'Benutzername wird benötigt',
userNameLessThan30Chars: 'Benutzername muss kürzer als 30 Zeichen sein'
userNameLessThan30Chars: 'Benutzername muss kürzer als 30 Zeichen sein',
minLength: 'Benutzername muss mindestens 3 Zeichen lang sein'
},
roleRules: {
roleRequired: 'Rolle wird benötigt'
@ -23,12 +24,18 @@ export default {
fileFormatRules: {
fileFormatRequired: 'Dateiformat wird benötigt'
},
emailRules: {
required: 'E-Mail wird benötigt',
format: 'E-Mail muss gültig sein'
},
uploadFileRules: {
fileRequired: 'Datei(en) werden benötigt',
fileLessThan1MB: 'Dateigröße muss kleiner als 100 MB sein!'
},
passwordRules: {
passwordRequired: 'Passwort wird benötigt',
passwordLessThan30Chars: 'Passwort muss kürzer als 30 Zeichen sein'
passwordLessThan30Chars: 'Passwort muss kürzer als 30 Zeichen sein',
minLength: 'Passwort muss mindestens 8 Zeichen lang sein',
match: 'Passwörter müssen übereinstimmen',
}
}

3
frontend/i18n/de/user.js

@ -1,7 +1,10 @@
export default {
login: 'Einloggen',
register: 'Registrieren',
signOut: 'Ausloggen',
username: 'Benutzername',
password: 'Passwort',
email: 'E-Mail',
confirmPassword: 'Passwort bestätigen',
socialLogin: 'Anmeldung über {provider}'
}

3
frontend/i18n/en/header.js

@ -1,3 +1,4 @@
export default {
projects: 'projects'
projects: 'projects',
users: 'users',
}

13
frontend/i18n/en/rules.js

@ -12,7 +12,8 @@ export default {
},
userNameRules: {
userNameRequired: 'User name is required',
userNameLessThan30Chars: 'User name must be less than 30 characters'
userNameLessThan30Chars: 'User name must be less than 30 characters',
min : 'User name must be at least 3 characters long',
},
roleRules: {
roleRequired: 'Role is required'
@ -27,12 +28,18 @@ export default {
fileFormatRules: {
fileFormatRequired: 'File format is required'
},
emailRules: {
required: 'Email is required',
format: 'Email must be valid'
},
uploadFileRules: {
fileRequired: 'File is required',
fileLessThan1MB: 'File size should be less than 100 MB!'
},
passwordRules: {
passwordRequired: 'Password is required',
passwordLessThan30Chars: 'Password must be less than 30 characters'
required: 'Password is required',
passwordLessThan30Chars: 'Password must be less than 30 characters',
minLength: 'Password must be at least 8 characters long',
match: 'Passwords must match'
}
}

8
frontend/i18n/en/user.js

@ -1,7 +1,13 @@
export default {
login: 'Login',
register: 'Register',
signOut: 'Sign Out',
username: 'Username',
password: 'Password',
socialLogin: 'Login With {provider}'
email: 'Email',
confirmPassword: 'Confirm Password',
socialLogin: 'Login With {provider}',
superUser: 'Super User',
staff: 'Staff',
active: 'Active',
}

3
frontend/i18n/fr/header.js

@ -1,3 +1,4 @@
export default {
projects: 'projets'
projects: 'projets',
users: 'utilisateurs',
}

11
frontend/i18n/fr/rules.js

@ -12,7 +12,8 @@ export default {
},
userNameRules: {
userNameRequired: "Le nom d'utilisateur est requis",
userNameLessThan30Chars: "Le nom d'utilisateur doit comporter moins de 30 caractères"
userNameLessThan30Chars: "Le nom d'utilisateur doit comporter moins de 30 caractères",
minLength: 'Le nom d\'utilisateur doit comporter au moins 3 caractères'
},
roleRules: {
roleRequired: 'Le rôle est obligatoire'
@ -27,12 +28,18 @@ export default {
fileFormatRules: {
fileFormatRequired: 'Le format de fichier est requis'
},
emailRules: {
required: 'L\'e-mail est requis',
format: 'L\'e-mail doit être valide'
},
uploadFileRules: {
fileRequired: 'Le fichier est obligatoire',
fileLessThan1MB: 'La taille du fichier doit être inférieure à 100 MB'
},
passwordRules: {
passwordRequired: 'Le mot de passe est obligatoire',
passwordLessThan30Chars: 'Le mot de passe doit comporter moins de 30 caractères'
passwordLessThan30Chars: 'Le mot de passe doit comporter moins de 30 caractères',
minLength: 'Le mot de passe doit comporter au moins 8 caractères',
match: 'Les mots de passe doivent correspondre'
}
}

3
frontend/i18n/fr/user.js

@ -1,7 +1,10 @@
export default {
login: 'Connexion',
register: 'S\'inscrire',
signOut: 'Déconnexion',
username: "Nom d'utilisateur",
password: 'Mot de passe',
email: 'E-mail',
confirmPassword: 'Confirmer le mot de passe',
socialLogin: 'Connexion via {provider}'
}

3
frontend/i18n/zh/header.js

@ -1,3 +1,4 @@
export default {
projects: '项目'
projects: '项目',
users: '用户',
}

12
frontend/i18n/zh/rules.js

@ -12,7 +12,8 @@ export default {
},
userNameRules: {
userNameRequired: '请输入用户名',
userNameLessThan30Chars: '用户名必须少于30个字符'
userNameLessThan30Chars: '用户名必须少于30个字符',
minLength: '用户名至少需要3个字符'
},
roleRules: {
roleRequired: '请输入角色'
@ -27,12 +28,19 @@ export default {
fileFormatRules: {
fileFormatRequired: '请输入文件类型'
},
emailRules: {
required: '请输入邮箱',
format: '邮箱格式不正确'
},
uploadFileRules: {
fileRequired: '请输入文件',
fileLessThan1MB: '文件大小必须小于 100 MB!'
},
passwordRules: {
passwordRequired: '请输入密码',
passwordLessThan30Chars: '密码必须小于30个字符'
passwordLessThan30Chars: '密码必须小于30个字符',
required: '密码是必需的',
minLength: '密码至少需要8个字符',
match: '密码必须匹配'
}
}

5
frontend/i18n/zh/user.js

@ -3,5 +3,8 @@ export default {
signOut: '注销',
username: '用户名',
password: '密码',
socialLogin: '通过 {provider} 登录'
socialLogin: '通过 {provider} 登录',
register: '注册',
email: '电子邮件',
confirmPassword: '确认密码'
}

7
frontend/plugins/services.ts

@ -8,6 +8,7 @@ import { TagApplicationService } from '@/services/application/tag/tagApplication
import { BoundingBoxApplicationService } from '@/services/application/tasks/boundingBox/boundingBoxApplicationService'
import { SegmentationApplicationService } from '@/services/application/tasks/segmentation/segmentationApplicationService'
import { SequenceLabelingApplicationService } from '@/services/application/tasks/sequenceLabeling/sequenceLabelingApplicationService'
import { UserApplicationService } from '~/services/application/user/UserApplicationService'
export interface Services {
categoryType: LabelApplicationService
@ -19,7 +20,8 @@ export interface Services {
option: OptionApplicationService
tag: TagApplicationService
bbox: BoundingBoxApplicationService
segmentation: SegmentationApplicationService
segmentation: SegmentationApplicationService,
user: UserApplicationService
}
declare module 'vue/types/vue' {
@ -42,7 +44,8 @@ const plugin: Plugin = (_, inject) => {
option: new OptionApplicationService(repositories.option),
tag: new TagApplicationService(repositories.tag),
bbox: new BoundingBoxApplicationService(repositories.boundingBox),
segmentation: new SegmentationApplicationService(repositories.segmentation)
segmentation: new SegmentationApplicationService(repositories.segmentation),
user: new UserApplicationService(repositories.user),
}
inject('services', services)
}

10
frontend/repositories/auth/apiAuthRepository.ts

@ -18,4 +18,14 @@ export class APIAuthRepository {
const response = await this.request.get(url)
return response.data
}
async register(
username: string,
email: string,
password1: string,
password2: string
): Promise<void> {
const url = '/users/create/'
await this.request.post(url, { username, email, password1, password2 })
}
}

71
frontend/repositories/user/apiUserRepository.ts

@ -1,22 +1,77 @@
import { UserItem } from '@/domain/models/user/user'
import ApiService from '@/services/api.service'
import { User, UserDetails } from '@/domain/models/user/user'
function toModel(item: { [key: string]: any }): UserItem {
return new UserItem(item.id, item.username, item.is_superuser, item.is_staff)
export interface PaginatedResponse<T> {
count: number
next: string | null
previous: string | null
results: T[]
}
function toModel(item: { [key: string]: any }): User {
return new User(
item.id,
item.username,
item.email,
item.is_active,
item.is_superuser,
item.is_staff
)
}
function toModelDetails(item: { [key: string]: any }): UserDetails {
return new UserDetails(
item.id,
item.username,
item.email,
item.first_name,
item.last_name,
item.is_active,
item.is_superuser,
item.is_staff,
item.date_joined
)
}
function toModelList(response: PaginatedResponse<any>): PaginatedResponse<User> {
return {
count: response.count,
next: response.next,
previous: response.previous,
results: response.results.map((item: any) => toModel(item))
}
}
export class APIUserRepository {
constructor(private readonly request = ApiService) {}
constructor(private readonly request = ApiService) { }
async getProfile(): Promise<UserItem> {
async getProfile(): Promise<User> {
const url = '/me'
const response = await this.request.get(url)
return toModel(response.data)
}
async list(query: string): Promise<UserItem[]> {
const url = `/users?q=${query}`
async list(query: string): Promise<PaginatedResponse<User>> {
const url = `/users?${query}`
const response = await this.request.get(url)
return response.data.map((item: { [key: string]: any }) => toModel(item))
return toModelList(response.data);
}
async getUser(id: number): Promise<UserDetails> {
const url = `/users/${id}`
const response = await this.request.get(url)
return toModelDetails(response.data)
}
async updateUser(id: number, data: { [key: string]: any }): Promise<User> {
const url = `/users/${id}`
const response = await this.request.patch(url, data)
return toModel(response.data)
}
async deleteUser(id: number): Promise<void> {
const url = `/users/${id}`
await this.request.delete(url)
}
}

16
frontend/rules/index.js

@ -11,7 +11,8 @@ export const labelNameRules = (msg) => {
export const userNameRules = (msg) => {
return [
(v) => !!v || msg.userNameRequired,
(v) => (v && v.length <= 30) || msg.userNameLessThan30Chars
(v) => (v && v.length <= 30) || msg.userNameLessThan30Chars,
(v) => (v && v.length >= 3) || msg.minLength
]
}
@ -39,10 +40,21 @@ export const uploadSingleFileRules = (msg) => {
export const passwordRules = (msg) => {
return [
(v) => !!v || msg.passwordRequired,
(v) => (v && v.length <= 30) || msg.passwordLessThan30Chars
(v) => (v && v.length <= 30) || msg.passwordLessThan30Chars,
(v) => (v && v.length >= 8) || msg.minLength,
]
}
export const templateNameRules = () => {
return [(v) => !!v || 'Name is required']
}
export const emailRules = (msg) => {
return [
(v) => !!v || msg.required,
(v) => {
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return pattern.test(v) || msg.format
}
]
}

21
frontend/store/auth.js

@ -20,6 +20,9 @@ export const mutations = {
},
setIsStaff(state, isStaff) {
state.isStaff = isStaff
},
setIsSuperUser(state, isSuperUser) {
state.isSuperUser = isSuperUser
}
}
@ -35,11 +38,15 @@ export const getters = {
},
isStaff(state) {
return state.isStaff
},
isSuperUser(state) {
return state.isSuperUser
}
}
export const actions = {
async authenticateUser({ commit }, authData) {
console.log('authData', authData)
try {
await this.$repositories.auth.login(authData.username, authData.password)
commit('setAuthenticated', true)
@ -57,6 +64,7 @@ export const actions = {
commit('setUsername', user.username)
commit('setUserId', user.id)
commit('setIsStaff', user.isStaff)
commit('setIsSuperUser', user.isSuperUser)
} catch {
commit('setAuthenticated', false)
commit('setIsStaff', false)
@ -67,5 +75,18 @@ export const actions = {
commit('setAuthenticated', false)
commit('setIsStaff', false)
commit('clearUsername')
},
async registerUser(_,authData) {
console.log('authData', authData)
try {
await this.$repositories.auth.register(
authData.username,
authData.email,
authData.password1,
authData.password2
)
} catch (error) {
throw new Error('Failed to register user')
}
}
}
Loading…
Cancel
Save