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.
+
+
+
+# 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
+
diff --git a/pinry-spa/src/components/UserProfileCard.vue b/pinry-spa/src/components/UserProfileCard.vue
index 1d78ca8..dd514ec 100644
--- a/pinry-spa/src/components/UserProfileCard.vue
+++ b/pinry-spa/src/components/UserProfileCard.vue
@@ -47,6 +47,16 @@
Boards
+
+
+
+
+ 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 @@
+
+
+
+
+
+
+
Your Token is:
+
{{ token }}
+ Please read
DRF API Documentation for more detail to know how to use it.
+
+
+
+
+
+
+
+
+
+
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):