Include auth token and generate auth token
This commit is contained in:
parent
f948113c0f
commit
f90bd4722c
@ -158,6 +158,10 @@ The REST api provides three different forms of authentication.
|
|||||||
|
|
||||||
3. Token authentication
|
3. Token authentication
|
||||||
|
|
||||||
|
You can create (or re-create) an API token by opening the "My Profile"
|
||||||
|
link in the user dropdown found in the web UI and clicking the circular
|
||||||
|
arrow button.
|
||||||
|
|
||||||
Paperless also offers an endpoint to acquire authentication tokens.
|
Paperless also offers an endpoint to acquire authentication tokens.
|
||||||
|
|
||||||
POST a username and password as a form or json string to
|
POST a username and password as a form or json string to
|
||||||
@ -169,7 +173,7 @@ The REST api provides three different forms of authentication.
|
|||||||
Authorization: Token <token>
|
Authorization: Token <token>
|
||||||
```
|
```
|
||||||
|
|
||||||
Tokens can be managed and revoked in the paperless admin.
|
Tokens can also be managed in the Django admin.
|
||||||
|
|
||||||
## Searching for documents
|
## Searching for documents
|
||||||
|
|
||||||
|
@ -27,6 +27,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" i18n>API Auth Token</label>
|
||||||
|
<div class="position-relative">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" formControlName="auth_token" readonly>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
|
||||||
|
<svg class="buttonicon-sm" fill="currentColor">
|
||||||
|
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
|
||||||
|
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
|
||||||
|
</svg><span class="visually-hidden" i18n>Copy</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
|
||||||
|
<svg class="buttonicon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
@ -3,3 +3,7 @@
|
|||||||
margin: 0 !important; // hack-ish, for animation
|
margin: 0 !important; // hack-ish, for animation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copied-badge {
|
||||||
|
right: 8em;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
fakeAsync,
|
||||||
|
tick,
|
||||||
|
} from '@angular/core/testing'
|
||||||
|
|
||||||
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
|
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
|
||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
@ -6,9 +11,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|||||||
import {
|
import {
|
||||||
NgbAccordionModule,
|
NgbAccordionModule,
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
NgbModal,
|
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbModule,
|
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { HttpClientModule } from '@angular/common/http'
|
import { HttpClientModule } from '@angular/common/http'
|
||||||
import { TextComponent } from '../input/text/text.component'
|
import { TextComponent } from '../input/text/text.component'
|
||||||
@ -16,12 +19,14 @@ import { PasswordComponent } from '../input/password/password.component'
|
|||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { Clipboard } from '@angular/cdk/clipboard'
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
email: 'foo@bar.com',
|
email: 'foo@bar.com',
|
||||||
password: '*********',
|
password: '*********',
|
||||||
first_name: 'foo',
|
first_name: 'foo',
|
||||||
last_name: 'bar',
|
last_name: 'bar',
|
||||||
|
auth_token: '123456789abcdef',
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ProfileEditDialogComponent', () => {
|
describe('ProfileEditDialogComponent', () => {
|
||||||
@ -29,6 +34,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
let fixture: ComponentFixture<ProfileEditDialogComponent>
|
let fixture: ComponentFixture<ProfileEditDialogComponent>
|
||||||
let profileService: ProfileService
|
let profileService: ProfileService
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
|
let clipboard: Clipboard
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -48,6 +54,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
profileService = TestBed.inject(ProfileService)
|
profileService = TestBed.inject(ProfileService)
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
|
clipboard = TestBed.inject(Clipboard)
|
||||||
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@ -68,6 +75,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
password: profile.password,
|
password: profile.password,
|
||||||
first_name: 'foo2',
|
first_name: 'foo2',
|
||||||
last_name: profile.last_name,
|
last_name: profile.last_name,
|
||||||
|
auth_token: profile.auth_token,
|
||||||
}
|
}
|
||||||
const updateSpy = jest.spyOn(profileService, 'update')
|
const updateSpy = jest.spyOn(profileService, 'update')
|
||||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
@ -151,4 +159,39 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
)
|
)
|
||||||
expect(component.saveDisabled).toBeFalsy()
|
expect(component.saveDisabled).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support auth token copy', fakeAsync(() => {
|
||||||
|
const getSpy = jest.spyOn(profileService, 'get')
|
||||||
|
getSpy.mockReturnValue(of(profile))
|
||||||
|
component.ngOnInit()
|
||||||
|
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||||
|
component.copyAuthToken()
|
||||||
|
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
|
||||||
|
expect(component.copied).toBeTruthy()
|
||||||
|
tick(3000)
|
||||||
|
expect(component.copied).toBeFalsy()
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('should support generate token, display error if needed', () => {
|
||||||
|
const getSpy = jest.spyOn(profileService, 'get')
|
||||||
|
getSpy.mockReturnValue(of(profile))
|
||||||
|
|
||||||
|
const generateSpy = jest.spyOn(profileService, 'generateAuthToken')
|
||||||
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
generateSpy.mockReturnValueOnce(
|
||||||
|
throwError(() => new Error('failed to generate'))
|
||||||
|
)
|
||||||
|
component.generateAuthToken()
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
generateSpy.mockClear()
|
||||||
|
const newToken = '789101112hijk'
|
||||||
|
generateSpy.mockReturnValueOnce(of(newToken))
|
||||||
|
component.generateAuthToken()
|
||||||
|
expect(generateSpy).toHaveBeenCalled()
|
||||||
|
expect(component.form.get('auth_token').value).not.toEqual(
|
||||||
|
profile.auth_token
|
||||||
|
)
|
||||||
|
expect(component.form.get('auth_token').value).toEqual(newToken)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -4,6 +4,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
|
import { Clipboard } from '@angular/cdk/clipboard'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-profile-edit-dialog',
|
selector: 'pngx-profile-edit-dialog',
|
||||||
@ -22,6 +23,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
|||||||
password_confirm: new FormControl({ value: null, disabled: true }),
|
password_confirm: new FormControl({ value: null, disabled: true }),
|
||||||
first_name: new FormControl(''),
|
first_name: new FormControl(''),
|
||||||
last_name: new FormControl(''),
|
last_name: new FormControl(''),
|
||||||
|
auth_token: new FormControl(''),
|
||||||
})
|
})
|
||||||
|
|
||||||
private currentPassword: string
|
private currentPassword: string
|
||||||
@ -34,10 +36,13 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
|||||||
private emailConfirm: string
|
private emailConfirm: string
|
||||||
public showEmailConfirm: boolean = false
|
public showEmailConfirm: boolean = false
|
||||||
|
|
||||||
|
public copied: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private profileService: ProfileService,
|
private profileService: ProfileService,
|
||||||
public activeModal: NgbActiveModal,
|
public activeModal: NgbActiveModal,
|
||||||
private toastService: ToastService
|
private toastService: ToastService,
|
||||||
|
private clipboard: Clipboard
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -70,17 +75,17 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
|||||||
return this.error?.password_confirm || this.error?.email_confirm
|
return this.error?.password_confirm || this.error?.email_confirm
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmailKeyUp(event: KeyboardEvent) {
|
onEmailKeyUp(event: KeyboardEvent): void {
|
||||||
this.newEmail = (event.target as HTMLInputElement)?.value
|
this.newEmail = (event.target as HTMLInputElement)?.value
|
||||||
this.onEmailChange()
|
this.onEmailChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmailConfirmKeyUp(event: KeyboardEvent) {
|
onEmailConfirmKeyUp(event: KeyboardEvent): void {
|
||||||
this.emailConfirm = (event.target as HTMLInputElement)?.value
|
this.emailConfirm = (event.target as HTMLInputElement)?.value
|
||||||
this.onEmailChange()
|
this.onEmailChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmailChange() {
|
onEmailChange(): void {
|
||||||
this.showEmailConfirm = this.currentEmail !== this.newEmail
|
this.showEmailConfirm = this.currentEmail !== this.newEmail
|
||||||
if (this.showEmailConfirm) {
|
if (this.showEmailConfirm) {
|
||||||
this.form.get('email_confirm').enable()
|
this.form.get('email_confirm').enable()
|
||||||
@ -96,19 +101,18 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordKeyUp(event: KeyboardEvent) {
|
onPasswordKeyUp(event: KeyboardEvent): void {
|
||||||
this.newPassword = (event.target as HTMLInputElement)?.value
|
this.newPassword = (event.target as HTMLInputElement)?.value
|
||||||
this.onPasswordChange()
|
this.onPasswordChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordConfirmKeyUp(event: KeyboardEvent) {
|
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
|
||||||
this.passwordConfirm = (event.target as HTMLInputElement)?.value
|
this.passwordConfirm = (event.target as HTMLInputElement)?.value
|
||||||
this.onPasswordChange()
|
this.onPasswordChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordChange() {
|
onPasswordChange(): void {
|
||||||
this.showPasswordConfirm = this.currentPassword !== this.newPassword
|
this.showPasswordConfirm = this.currentPassword !== this.newPassword
|
||||||
console.log(this.currentPassword, this.newPassword, this.passwordConfirm)
|
|
||||||
|
|
||||||
if (this.showPasswordConfirm) {
|
if (this.showPasswordConfirm) {
|
||||||
this.form.get('password_confirm').enable()
|
this.form.get('password_confirm').enable()
|
||||||
@ -124,7 +128,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save(): void {
|
||||||
const profile = Object.assign({}, this.form.value)
|
const profile = Object.assign({}, this.form.value)
|
||||||
this.networkActive = true
|
this.networkActive = true
|
||||||
this.profileService
|
this.profileService
|
||||||
@ -142,7 +146,30 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel(): void {
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateAuthToken(): void {
|
||||||
|
this.profileService.generateAuthToken().subscribe({
|
||||||
|
next: (token: string) => {
|
||||||
|
console.log(token)
|
||||||
|
this.form.patchValue({ auth_token: token })
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error generating auth token`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
copyAuthToken(): void {
|
||||||
|
this.clipboard.copy(this.form.get('auth_token').value)
|
||||||
|
this.copied = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copied = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,4 +3,5 @@ export interface PaperlessUserProfile {
|
|||||||
password?: string
|
password?: string
|
||||||
first_name?: string
|
first_name?: string
|
||||||
last_name?: string
|
last_name?: string
|
||||||
|
auth_token?: string
|
||||||
}
|
}
|
||||||
|
@ -43,4 +43,12 @@ describe('ProfileService', () => {
|
|||||||
email: 'foo@bar.com',
|
email: 'foo@bar.com',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('supports generating new auth token', () => {
|
||||||
|
service.generateAuthToken().subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}profile/generate_auth_token/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('POST')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -24,4 +24,11 @@ export class ProfileService {
|
|||||||
profile
|
profile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateAuthToken(): Observable<string> {
|
||||||
|
return this.http.post<string>(
|
||||||
|
`${environment.apiBaseUrl}${this.endpoint}/generate_auth_token/`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ from guardian.shortcuts import assign_perm
|
|||||||
from guardian.shortcuts import get_perms
|
from guardian.shortcuts import get_perms
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
from whoosh.writing import AsyncWriter
|
from whoosh.writing import AsyncWriter
|
||||||
|
|
||||||
@ -5823,7 +5824,6 @@ class TestApiProfile(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
self.assertEqual(response.data["email"], self.user.email)
|
self.assertEqual(response.data["email"], self.user.email)
|
||||||
self.assertEqual(response.data["password"], "**********")
|
|
||||||
self.assertEqual(response.data["first_name"], self.user.first_name)
|
self.assertEqual(response.data["first_name"], self.user.first_name)
|
||||||
self.assertEqual(response.data["last_name"], self.user.last_name)
|
self.assertEqual(response.data["last_name"], self.user.last_name)
|
||||||
|
|
||||||
@ -5852,3 +5852,26 @@ class TestApiProfile(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(user.email, user_data["email"])
|
self.assertEqual(user.email, user_data["email"])
|
||||||
self.assertEqual(user.first_name, user_data["first_name"])
|
self.assertEqual(user.first_name, user_data["first_name"])
|
||||||
self.assertEqual(user.last_name, user_data["last_name"])
|
self.assertEqual(user.last_name, user_data["last_name"])
|
||||||
|
|
||||||
|
def test_update_auth_token(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Configured user
|
||||||
|
WHEN:
|
||||||
|
- API call is made to generate auth token
|
||||||
|
THEN:
|
||||||
|
- Token is created the first time, updated the second
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.assertEqual(len(Token.objects.all()), 0)
|
||||||
|
|
||||||
|
response = self.client.post(f"{self.ENDPOINT}generate_auth_token/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
token1 = Token.objects.filter(user=self.user).first()
|
||||||
|
self.assertIsNotNone(token1)
|
||||||
|
|
||||||
|
response = self.client.post(f"{self.ENDPOINT}generate_auth_token/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
token2 = Token.objects.filter(user=self.user).first()
|
||||||
|
|
||||||
|
self.assertNotEqual(token1.key, token2.key)
|
||||||
|
@ -101,7 +101,8 @@ class GroupSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class ProfileSerializer(serializers.ModelSerializer):
|
class ProfileSerializer(serializers.ModelSerializer):
|
||||||
email = serializers.EmailField(allow_null=False)
|
email = serializers.EmailField(allow_null=False)
|
||||||
password = ObfuscatedUserPasswordField(required=False)
|
password = ObfuscatedUserPasswordField(required=False, allow_null=False)
|
||||||
|
auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@ -110,4 +111,5 @@ class ProfileSerializer(serializers.ModelSerializer):
|
|||||||
"password",
|
"password",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
|
"auth_token",
|
||||||
)
|
)
|
||||||
|
@ -35,6 +35,7 @@ from documents.views import UiSettingsView
|
|||||||
from documents.views import UnifiedSearchViewSet
|
from documents.views import UnifiedSearchViewSet
|
||||||
from paperless.consumers import StatusConsumer
|
from paperless.consumers import StatusConsumer
|
||||||
from paperless.views import FaviconView
|
from paperless.views import FaviconView
|
||||||
|
from paperless.views import GenerateAuthTokenView
|
||||||
from paperless.views import GroupViewSet
|
from paperless.views import GroupViewSet
|
||||||
from paperless.views import ProfileView
|
from paperless.views import ProfileView
|
||||||
from paperless.views import UserViewSet
|
from paperless.views import UserViewSet
|
||||||
@ -120,6 +121,7 @@ urlpatterns = [
|
|||||||
BulkEditObjectPermissionsView.as_view(),
|
BulkEditObjectPermissionsView.as_view(),
|
||||||
name="bulk_edit_object_permissions",
|
name="bulk_edit_object_permissions",
|
||||||
),
|
),
|
||||||
|
path("profile/generate_auth_token/", GenerateAuthTokenView.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
"^profile/",
|
"^profile/",
|
||||||
ProfileView.as_view(),
|
ProfileView.as_view(),
|
||||||
|
@ -7,6 +7,7 @@ from django.db.models.functions import Lower
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
@ -111,20 +112,17 @@ class GroupViewSet(ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ProfileView(GenericAPIView):
|
class ProfileView(GenericAPIView):
|
||||||
|
"""
|
||||||
|
User profile view, only available when logged in
|
||||||
|
"""
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
serializer_class = ProfileSerializer
|
serializer_class = ProfileSerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
user = self.request.user if hasattr(self.request, "user") else None
|
user = self.request.user if hasattr(self.request, "user") else None
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
return Response(
|
return Response(serializer.to_representation(user))
|
||||||
{
|
|
||||||
"email": user.email,
|
|
||||||
"password": "**********",
|
|
||||||
"first_name": user.first_name,
|
|
||||||
"last_name": user.last_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
@ -139,11 +137,25 @@ class ProfileView(GenericAPIView):
|
|||||||
for key, value in serializer.validated_data.items():
|
for key, value in serializer.validated_data.items():
|
||||||
setattr(user, key, value)
|
setattr(user, key, value)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
return Response(serializer.to_representation(user))
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateAuthTokenView(GenericAPIView):
|
||||||
|
"""
|
||||||
|
Generates (or re-generates) an auth token, requires a logged in user
|
||||||
|
unlike the default DRF endpoint
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
user = self.request.user if hasattr(self.request, "user") else None
|
||||||
|
|
||||||
|
existing_token = Token.objects.filter(user=user).first()
|
||||||
|
if existing_token is not None:
|
||||||
|
existing_token.delete()
|
||||||
|
token = Token.objects.create(user=user)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
token.key,
|
||||||
"email": user.email,
|
|
||||||
"password": "**********",
|
|
||||||
"first_name": user.first_name,
|
|
||||||
"last_name": user.last_name,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user