diff --git a/README.md b/README.md index fe028a8..63321eb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ For more information ( screenshots and document ) visit [getpinry.com](https://g ## Features - - Image fetch and online preview - Tagging system for Pins - Browser Extensions @@ -20,6 +19,7 @@ For more information ( screenshots and document ) visit [getpinry.com](https://g - Works well with docker - Both public and private boards (add @2020.02.11) - Search by tags / Search boards with name (add @2020.02.14) +- Full API support via DRF (add @2022.02.19) ## Install with Docker See our full documentation at [https://docs.getpinry.com/install-with-docker/](https://docs.getpinry.com/install-with-docker/) diff --git a/core/tests/api.py b/core/tests/api.py index 6076e9b..71d11ce 100644 --- a/core/tests/api.py +++ b/core/tests/api.py @@ -32,7 +32,7 @@ class ImageTests(APITestCase): data=data, format='json', ) - self.assertEqual(response.status_code, 403, response.data) + self.assertEqual(response.status_code, 401, response.data) class BoardPrivacyTests(APITestCase): @@ -220,7 +220,7 @@ class PinTests(APITestCase): self.client.logout() uri = reverse("pin-detail", kwargs={"pk": pin.pk}) response = self.client.patch(uri, format='json', data={}) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 401, response.data) def test_patch_detail(self): image = create_image() @@ -240,7 +240,8 @@ class PinTests(APITestCase): pin = create_pin(self.user, image, []) uri = reverse("pin-detail", kwargs={"pk": pin.pk}) self.client.logout() - self.assertEqual(self.client.delete(uri).status_code, 403) + resp = self.client.delete(uri) + self.assertEqual(resp.status_code, 401, resp.data) def test_delete_detail(self): image = create_image() diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..ee23f5a --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,30 @@ +# API Support + +Pinry support API accessing via token so that you could write your own client or import any pin format to your instance. + +# Get Token +Just go to `My -> Profile` page to get the token. + +![token-accessing](./imgs/screenshots/token-access.png) + +# Access API via Token + +Here is an example for curl to access `user-profile` API: + + curl -X GET http://192.168.1.101:8080/api/v2/profile/users/ -H 'Authorization: Token fa3b0ed2b8a87c81323688c288642288c9570aca' + +You will get response like this: + + [ + { + "username": "winkidney", + "token": "fa3b0ed2b8a87c81323688c288642288c9570aca", + "email": "winkidney@gmail.com", + "gravatar": "0d7161ac663cdb21108502cd4051149c", + "resource_link": "http://localhost:8000/api/v2/profile/users/1/" + } + ] + + +# API reference +Just use the interactive API interface by DRF or follow the file [api.js](https://github.com/pinry/pinry/blob/master/pinry-spa/src/components/api.js) diff --git a/docs/src/imgs/screenshots/token-access.png b/docs/src/imgs/screenshots/token-access.png new file mode 100644 index 0000000..f6546dc Binary files /dev/null and b/docs/src/imgs/screenshots/token-access.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 8135da6..1198fb8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: - Theories: 'theories.md' - Install with Docker: 'install-with-docker.md' - Development: 'development.md' + - API support: 'api.md' - Plugin System: 'plugin-system.md' - Upgrade Guide: 'upgrade-guide.md' - Docs: 'docs.md' diff --git a/pinry-spa/src/components/PHeader.vue b/pinry-spa/src/components/PHeader.vue index 8c725ea..7bfd036 100644 --- a/pinry-spa/src/components/PHeader.vue +++ b/pinry-spa/src/components/PHeader.vue @@ -56,6 +56,11 @@ class="navbar-item"> Pins + + Profile + @@ -71,6 +81,10 @@ export default { type: Boolean, default: false, }, + inProfile: { + type: Boolean, + default: false, + }, }, data() { return { @@ -91,6 +105,11 @@ export default { { name: 'boards4user', params: { username: this.username } }, ); }, + go2UserProfile() { + this.$router.push( + { name: 'profile4user', params: { username: this.username } }, + ); + }, go2UserPins() { this.$router.push( { name: 'user', params: { user: this.username } }, diff --git a/pinry-spa/src/components/user/profile.vue b/pinry-spa/src/components/user/profile.vue new file mode 100644 index 0000000..fefdec8 --- /dev/null +++ b/pinry-spa/src/components/user/profile.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/pinry-spa/src/router/index.js b/pinry-spa/src/router/index.js index 8838324..8f01d90 100644 --- a/pinry-spa/src/router/index.js +++ b/pinry-spa/src/router/index.js @@ -6,6 +6,7 @@ import Pins4User from '../views/Pins4User.vue'; import Pins4Board from '../views/Pins4Board.vue'; import Pins4Id from '../views/Pins4Id.vue'; import Boards4User from '../views/Boards4User.vue'; +import Profile4User from '../views/Profile4User.vue'; import PinCreate from '../views/PinCreate.vue'; import Search from '../views/Search.vue'; import PageNotFound from '../views/PageNotFound.vue'; @@ -43,6 +44,11 @@ const routes = [ name: 'boards4user', component: Boards4User, }, + { + path: '/profile/:username', + name: 'profile4user', + component: Profile4User, + }, { path: '/pin-creation/from-url', name: 'pin-creation-from-url', diff --git a/pinry-spa/src/views/Profile4User.vue b/pinry-spa/src/views/Profile4User.vue new file mode 100644 index 0000000..7237fd3 --- /dev/null +++ b/pinry-spa/src/views/Profile4User.vue @@ -0,0 +1,54 @@ + + + + + + diff --git a/pinry/settings/base.py b/pinry/settings/base.py index 700f6ce..2387c6a 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -15,6 +15,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'rest_framework.authtoken', 'django_filters', 'taggit', 'compressor', @@ -163,4 +164,9 @@ REST_FRAMEWORK = { 'URL_FIELD_NAME': DRF_URL_FIELD_NAME, 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'PAGE_SIZE': API_LIMIT_PER_PAGE, + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ] } diff --git a/users/models.py b/users/models.py index cc52fa2..753732f 100644 --- a/users/models.py +++ b/users/models.py @@ -1,12 +1,29 @@ import hashlib from django.contrib.auth.models import User as BaseUser +from django.db.models.signals import post_save +from django.dispatch import receiver + + +def create_token_if_necessary(user: BaseUser): + from rest_framework.authtoken.models import Token + token = Token.objects.filter(user=user).first() + if token is not None: + return token + else: + return Token.objects.create(user=user) class User(BaseUser): + @property def gravatar(self): return hashlib.md5(self.email.encode('utf-8')).hexdigest() class Meta: proxy = True + + +@receiver(post_save, sender=User) +def create_profile(sender, instance: User, **kwargs): + create_token_if_necessary(instance) diff --git a/users/serializers.py b/users/serializers.py index 51b4a64..d2e02ff 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -3,7 +3,7 @@ from django.contrib.auth import login from rest_framework import serializers from rest_framework.exceptions import ValidationError -from users.models import User +from users.models import User, create_token_if_necessary class PublicUserSerializer(serializers.HyperlinkedModelSerializer): @@ -26,6 +26,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): model = User fields = ( 'username', + 'token', 'email', 'gravatar', 'password', @@ -52,6 +53,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): min_length=6, max_length=32, ) + token = serializers.SerializerMethodField(read_only=True) def create(self, validated_data): if validated_data['password'] != validated_data['password_repeat']: @@ -73,3 +75,6 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): backend=settings.AUTHENTICATION_BACKENDS[0], ) return user + + def get_token(self, obj: User): + return create_token_if_necessary(obj).key diff --git a/users/tests.py b/users/tests.py index 7149cb3..b1f3ce4 100644 --- a/users/tests.py +++ b/users/tests.py @@ -61,7 +61,7 @@ class CreateUserTest(TestCase): reverse('users:user-list'), data=data, ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 401) class LogoutViewTest(TestCase):