mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-17 15:53:28 +00:00
[api] use device specific token
This commit is contained in:
@@ -1,7 +1,21 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
from rest_framework.authentication import BaseAuthentication
|
from rest_framework.authentication import BaseAuthentication
|
||||||
|
|
||||||
from models import Token
|
|
||||||
from seahub.base.accounts import User
|
from seahub.base.accounts import User
|
||||||
|
from seahub.api2.models import Token, TokenV2
|
||||||
|
from seahub.api2.utils import get_client_ip
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def within_ten_min(d1, d2):
|
||||||
|
'''Return true if two datetime.datetime object differs less than ten minutes'''
|
||||||
|
delta = d2 - d1
|
||||||
|
interval = 60 * 10
|
||||||
|
return abs(delta.total_seconds()) < interval
|
||||||
|
|
||||||
|
HEADER_CLIENT_VERSION = 'HTTP_SEAFILE_CLEINT_VERSION'
|
||||||
|
HEADER_PLATFORM_VERSION = 'HTTP_SEAFILE_PLATFORM_VERSION'
|
||||||
|
|
||||||
class TokenAuthentication(BaseAuthentication):
|
class TokenAuthentication(BaseAuthentication):
|
||||||
"""
|
"""
|
||||||
@@ -11,10 +25,7 @@ class TokenAuthentication(BaseAuthentication):
|
|||||||
HTTP header, prepended with the string "Token ". For example:
|
HTTP header, prepended with the string "Token ". For example:
|
||||||
|
|
||||||
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
|
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
|
||||||
"""
|
|
||||||
|
|
||||||
model = Token
|
|
||||||
"""
|
|
||||||
A custom token model may be used, but must have the following properties.
|
A custom token model may be used, but must have the following properties.
|
||||||
|
|
||||||
* key -- The string identifying the token
|
* key -- The string identifying the token
|
||||||
@@ -23,17 +34,70 @@ class TokenAuthentication(BaseAuthentication):
|
|||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
|
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
|
||||||
|
key = None
|
||||||
if len(auth) == 2 and auth[0].lower() == "token":
|
if len(auth) == 2 and auth[0].lower() == "token":
|
||||||
key = auth[1]
|
key = auth[1]
|
||||||
try:
|
|
||||||
token = self.model.objects.get(key=key)
|
|
||||||
except self.model.DoesNotExist:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
user = User.objects.get(email=token.user)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
return None
|
|
||||||
if user.is_active:
|
|
||||||
return (user, token)
|
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret = self.authenticate_v2(request, key)
|
||||||
|
if ret:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return self.authenticate_v1(request, key)
|
||||||
|
|
||||||
|
def authenticate_v1(self, request, key):
|
||||||
|
try:
|
||||||
|
token = Token.objects.get(key=key)
|
||||||
|
except Token.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=token.user)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if user.is_active:
|
||||||
|
return (user, token)
|
||||||
|
|
||||||
|
def authenticate_v2(self, request, key):
|
||||||
|
try:
|
||||||
|
token = TokenV2.objects.get(key=key)
|
||||||
|
except TokenV2.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=token.user)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if user.is_active:
|
||||||
|
need_save = False
|
||||||
|
|
||||||
|
# We update the device's last_login_ip, client_version, platform_version if changed
|
||||||
|
ip = get_client_ip(request)
|
||||||
|
if ip and ip != token.last_login_ip:
|
||||||
|
token.last_login_ip = ip
|
||||||
|
need_save = True
|
||||||
|
|
||||||
|
client_version = request.META.get(HEADER_CLIENT_VERSION, '')
|
||||||
|
if client_version and client_version != token.client_version:
|
||||||
|
token.client_version = client_version
|
||||||
|
need_save = True
|
||||||
|
|
||||||
|
platform_version = request.META.get(HEADER_PLATFORM_VERSION, '')
|
||||||
|
if platform_version and platform_version != token.platform_version:
|
||||||
|
token.platform_version = platform_version
|
||||||
|
need_save = True
|
||||||
|
|
||||||
|
if not within_ten_min(token.last_accessed, datetime.datetime.now()):
|
||||||
|
# We only need 10min precision for the last_accessed field
|
||||||
|
need_save = True
|
||||||
|
|
||||||
|
if need_save:
|
||||||
|
try:
|
||||||
|
token.save()
|
||||||
|
except:
|
||||||
|
logger.exception('error when save token v2:')
|
||||||
|
return (user, token)
|
@@ -3,9 +3,10 @@ import hmac
|
|||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from seahub.base.accounts import User
|
|
||||||
from seahub.base.fields import LowerCaseCharField
|
from seahub.base.fields import LowerCaseCharField
|
||||||
|
|
||||||
|
DESKTOP_PLATFORMS = ('windows', 'linux', 'mac')
|
||||||
|
|
||||||
class Token(models.Model):
|
class Token(models.Model):
|
||||||
"""
|
"""
|
||||||
The default authorization token model.
|
The default authorization token model.
|
||||||
@@ -26,4 +27,127 @@ class Token(models.Model):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.key
|
return self.key
|
||||||
|
|
||||||
|
class TokenV2Manager(models.Manager):
|
||||||
|
def get_user_devices(self, username):
|
||||||
|
'''List user devices, most recently used first'''
|
||||||
|
devices = super(TokenV2Manager, self).filter(user=username)
|
||||||
|
|
||||||
|
platform_priorities = {
|
||||||
|
'windows': 0,
|
||||||
|
'linux': 0,
|
||||||
|
'mac': 0,
|
||||||
|
'android': 1,
|
||||||
|
'ios': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def sort_devices(d1, d2):
|
||||||
|
'''Desktop clients are listed before mobile clients. Devices of
|
||||||
|
the same category are listed by most recently used first
|
||||||
|
|
||||||
|
'''
|
||||||
|
ret = cmp(platform_priorities[d1.platform], platform_priorities[d2.platform])
|
||||||
|
if ret != 0:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return cmp(d2.last_accessed, d1.last_accessed)
|
||||||
|
|
||||||
|
return [ d.as_dict() for d in sorted(devices, sort_devices) ]
|
||||||
|
|
||||||
|
def _get_token_by_user_device(self, username, platform, device_id):
|
||||||
|
try:
|
||||||
|
return super(TokenV2Manager, self).get(user=username,
|
||||||
|
platform=platform,
|
||||||
|
device_id=device_id)
|
||||||
|
except TokenV2.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_or_create_token(self, username, platform, device_id, device_name,
|
||||||
|
client_version, platform_version, last_login_ip):
|
||||||
|
|
||||||
|
token = self._get_token_by_user_device(username, platform, device_id)
|
||||||
|
if token:
|
||||||
|
if token.client_version != client_version or token.platform_version != platform_version \
|
||||||
|
or token.device_name != device_name:
|
||||||
|
|
||||||
|
token.client_version = client_version
|
||||||
|
token.platform_version = platform_version
|
||||||
|
token.device_name = device_name
|
||||||
|
token.save()
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
token = TokenV2(user=username,
|
||||||
|
platform=platform,
|
||||||
|
device_id=device_id,
|
||||||
|
device_name=device_name,
|
||||||
|
client_version=client_version,
|
||||||
|
platform_version=platform_version,
|
||||||
|
last_login_ip=last_login_ip)
|
||||||
|
token.save()
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def delete_device_token(self, username, platform, device_id):
|
||||||
|
super(TokenV2Manager, self).filter(user=username, platform=platform, device_id=device_id).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class TokenV2(models.Model):
|
||||||
|
"""
|
||||||
|
Device specific token
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = models.CharField(max_length=40, primary_key=True)
|
||||||
|
|
||||||
|
user = LowerCaseCharField(max_length=255)
|
||||||
|
|
||||||
|
# windows/linux/mac/ios/android
|
||||||
|
platform = LowerCaseCharField(max_length=32)
|
||||||
|
|
||||||
|
# ccnet id, android secure id, etc.
|
||||||
|
device_id = models.CharField(max_length=40)
|
||||||
|
|
||||||
|
# lin-laptop
|
||||||
|
device_name = models.CharField(max_length=40)
|
||||||
|
|
||||||
|
# platform version
|
||||||
|
platform_version = LowerCaseCharField(max_length=16)
|
||||||
|
|
||||||
|
# seafile client/app version
|
||||||
|
client_version = LowerCaseCharField(max_length=16)
|
||||||
|
|
||||||
|
# most recent activity
|
||||||
|
last_accessed = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
last_login_ip = models.GenericIPAddressField(null=True, default=None)
|
||||||
|
|
||||||
|
objects = TokenV2Manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('user', 'platform', 'device_id'),)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.key:
|
||||||
|
self.key = self.generate_key()
|
||||||
|
return super(TokenV2, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def generate_key(self):
|
||||||
|
unique = str(uuid.uuid4())
|
||||||
|
return hmac.new(unique, digestmod=sha1).hexdigest()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return "TokenV2{user=%(user)s,device=%(device_name)s}" % \
|
||||||
|
dict(user=self.user,device_name=self.device_name)
|
||||||
|
|
||||||
|
def is_desktop_client(self):
|
||||||
|
return str(self.platform) in ('windows', 'linux', 'mac')
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return dict(key=self.key,
|
||||||
|
user=self.user,
|
||||||
|
platform=self.platform,
|
||||||
|
device_id=self.device_id,
|
||||||
|
device_name=self.device_name,
|
||||||
|
client_version=self.client_version,
|
||||||
|
platform_version=self.platform_version,
|
||||||
|
last_accessed=self.last_accessed,
|
||||||
|
last_login_ip=self.last_login_ip)
|
||||||
|
@@ -1,28 +1,103 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from seahub.auth import authenticate
|
from seahub.auth import authenticate
|
||||||
|
from seahub.api2.models import Token, TokenV2, DESKTOP_PLATFORMS
|
||||||
|
from seahub.api2.utils import get_client_ip
|
||||||
|
|
||||||
|
def all_none(values):
|
||||||
|
for value in values:
|
||||||
|
if value is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def all_not_none(values):
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
class AuthTokenSerializer(serializers.Serializer):
|
class AuthTokenSerializer(serializers.Serializer):
|
||||||
username = serializers.CharField()
|
username = serializers.CharField()
|
||||||
password = serializers.CharField()
|
password = serializers.CharField()
|
||||||
|
|
||||||
|
# There fields are used by TokenV2
|
||||||
|
platform = serializers.CharField(required=False)
|
||||||
|
device_id = serializers.CharField(required=False)
|
||||||
|
device_name = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
# These fields may be needed in the future
|
||||||
|
client_version = serializers.CharField(required=False)
|
||||||
|
platform_version = serializers.CharField(required=False)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
username = attrs.get('username')
|
username = attrs.get('username')
|
||||||
password = attrs.get('password')
|
password = attrs.get('password')
|
||||||
|
|
||||||
|
platform = attrs.get('platform', None)
|
||||||
|
device_id = attrs.get('device_id', None)
|
||||||
|
device_name = attrs.get('device_name', None)
|
||||||
|
client_version = attrs.get('client_version', None)
|
||||||
|
platform_version = attrs.get('platform_version', None)
|
||||||
|
|
||||||
|
v2_fields = (platform, device_id, device_name, client_version, platform_version)
|
||||||
|
|
||||||
|
# Decide the version of token we need
|
||||||
|
if all_none(v2_fields):
|
||||||
|
v2 = False
|
||||||
|
elif all_not_none(v2_fields):
|
||||||
|
v2 = True
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError('invalid params')
|
||||||
|
|
||||||
|
# first check password
|
||||||
if username and password:
|
if username and password:
|
||||||
user = authenticate(username=username, password=password)
|
user = authenticate(username=username, password=password)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise serializers.ValidationError('User account is disabled.')
|
raise serializers.ValidationError('User account is disabled.')
|
||||||
attrs['user'] = user
|
|
||||||
return attrs
|
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError('Must include "username" and "password"')
|
raise serializers.ValidationError('Must include "username" and "password"')
|
||||||
|
|
||||||
|
# Now user is authenticated
|
||||||
|
|
||||||
|
if v2:
|
||||||
|
token = self.get_token_v2(username, platform, device_id, device_name,
|
||||||
|
client_version, platform_version)
|
||||||
|
else:
|
||||||
|
token = self.get_token_v1(username)
|
||||||
|
return token.key
|
||||||
|
|
||||||
|
def get_token_v1(self, username):
|
||||||
|
token, created = Token.objects.get_or_create(user=username)
|
||||||
|
return token
|
||||||
|
|
||||||
|
def get_token_v2(self, username, platform, device_id, device_name,
|
||||||
|
client_version, platform_version):
|
||||||
|
|
||||||
|
if platform in DESKTOP_PLATFORMS:
|
||||||
|
# desktop device id is the peer id, so it must be 40 chars
|
||||||
|
if len(device_id) != 40:
|
||||||
|
raise serializers.ValidationError('invalid device id')
|
||||||
|
|
||||||
|
elif platform == 'android':
|
||||||
|
# android device id is the 64bit secure id, so it must be 16 chars in hex representation
|
||||||
|
if len(device_id) != 16:
|
||||||
|
raise serializers.ValidationError('invalid device id')
|
||||||
|
elif platform == 'ios':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError('invalid platform')
|
||||||
|
|
||||||
|
request = self.context['request']
|
||||||
|
last_login_ip = get_client_ip(request)
|
||||||
|
|
||||||
|
return TokenV2.objects.get_or_create_token(username, platform, device_id, device_name,
|
||||||
|
client_version, platform_version, last_login_ip)
|
||||||
|
|
||||||
class AccountSerializer(serializers.Serializer):
|
class AccountSerializer(serializers.Serializer):
|
||||||
email = serializers.EmailField()
|
email = serializers.EmailField()
|
||||||
password = serializers.CharField()
|
password = serializers.CharField()
|
||||||
|
@@ -449,3 +449,12 @@ def api_group_check(func):
|
|||||||
return api_error(status.HTTP_403_FORBIDDEN, 'Forbid to access this group.')
|
return api_error(status.HTTP_403_FORBIDDEN, 'Forbid to access this group.')
|
||||||
|
|
||||||
return _decorated
|
return _decorated
|
||||||
|
|
||||||
|
def get_client_ip(request):
|
||||||
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
|
||||||
|
if x_forwarded_for:
|
||||||
|
ip = x_forwarded_for.split(',')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR', '')
|
||||||
|
|
||||||
|
return ip
|
||||||
|
@@ -25,14 +25,13 @@ from django.template.loader import render_to_string
|
|||||||
from django.shortcuts import render_to_response
|
from django.shortcuts import render_to_response
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from models import Token
|
|
||||||
from authentication import TokenAuthentication
|
from authentication import TokenAuthentication
|
||||||
from serializers import AuthTokenSerializer, AccountSerializer
|
from serializers import AuthTokenSerializer, AccountSerializer
|
||||||
from utils import is_repo_writable, is_repo_accessible, calculate_repo_info, \
|
from utils import is_repo_writable, is_repo_accessible, calculate_repo_info, \
|
||||||
api_error, get_file_size, prepare_starred_files, \
|
api_error, get_file_size, prepare_starred_files, \
|
||||||
get_groups, get_group_and_contacts, prepare_events, \
|
get_groups, get_group_and_contacts, prepare_events, \
|
||||||
get_person_msgs, api_group_check, get_email, get_timetamp, \
|
get_person_msgs, api_group_check, get_email, get_timetamp, \
|
||||||
get_group_message_json, get_group_msgs, get_group_msgs_json
|
get_group_message_json, get_group_msgs, get_group_msgs_json, get_client_ip
|
||||||
from seahub.base.accounts import User
|
from seahub.base.accounts import User
|
||||||
from seahub.base.models import FileDiscuss, UserStarredFiles, \
|
from seahub.base.models import FileDiscuss, UserStarredFiles, \
|
||||||
DirFilesLastModifiedInfo, DeviceToken
|
DirFilesLastModifiedInfo, DeviceToken
|
||||||
@@ -131,13 +130,13 @@ class ObtainAuthToken(APIView):
|
|||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
|
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
|
||||||
renderer_classes = (renderers.JSONRenderer,)
|
renderer_classes = (renderers.JSONRenderer,)
|
||||||
model = Token
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = AuthTokenSerializer(data=request.DATA)
|
context = { 'request': request }
|
||||||
|
serializer = AuthTokenSerializer(data=request.DATA, context=context)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
token, created = Token.objects.get_or_create(user=serializer.object['user'].username)
|
key = serializer.object
|
||||||
return Response({'token': token.key})
|
return Response({'token': key})
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
95
seahub/templates/devices.html
Normal file
95
seahub/templates/devices.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{% extends "home_base.html" %}
|
||||||
|
|
||||||
|
{% load seahub_tags avatar_tags i18n %}
|
||||||
|
|
||||||
|
{% block sub_title %}{% trans "Devices" %} - {% endblock %}
|
||||||
|
{% block cur_devices %}tab-cur{% endblock %}
|
||||||
|
|
||||||
|
{% block right_panel %}
|
||||||
|
<h3 class="hd">{% trans "Devices" %}</h3>
|
||||||
|
{% if devices %}
|
||||||
|
<table class="client-list">
|
||||||
|
<tr>
|
||||||
|
<th width="10%">{% trans "Platform" %}</th>
|
||||||
|
<th width="25%">{% trans "Device Name" %}</th>
|
||||||
|
<th width="20%">{% trans "IP" %}</th>
|
||||||
|
<th width="15%">{% trans "Last Access" %}</th>
|
||||||
|
<th width="20%">{% trans "Libraries" %}</th>
|
||||||
|
<th width="10%">{% trans "Operation" %}</th>
|
||||||
|
</tr>
|
||||||
|
{% for device in devices %}
|
||||||
|
<tr data-platform="{{ device.platform }}" data-device-id="{{ device.device_id }}">
|
||||||
|
<td>{{ device.platform }}</td>
|
||||||
|
<td>{{ device.device_name }}</td>
|
||||||
|
<!-- <td>{{ device.client_version }}</td> -->
|
||||||
|
<td>{{ device.last_login_ip }}</td>
|
||||||
|
<td>{{ device.last_accessed | translate_seahub_time }}</td>
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
{% for repo in device.synced_repos %}
|
||||||
|
<li>
|
||||||
|
<a name="{{ repo.sync_time }}" href="{% url 'repo' repo.repo_id %}">{{ repo.repo_name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div><a href="#" class="unlink-device op vh">{% trans "Unlink" %}</a></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<div id="unlink-device-confirm" class="hide">
|
||||||
|
<p>{% trans "Really want to unlink this device? It will immediately stop syncing." %}</p>
|
||||||
|
<button class="yes">{% trans "Yes" %}</button>
|
||||||
|
<button class="no simplemodal-close">{% trans "No" %}</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-tips">
|
||||||
|
<h2 class="alc">{% trans "You do not have connected devices" %}</h2>
|
||||||
|
<p>{% trans "Your clients (Desktop/Anroid/iOS) would be listed here." %}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_script %}{{block.super}}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function send_unlink_request(tr) {
|
||||||
|
var post_data = {
|
||||||
|
'platform': tr.data('platform'),
|
||||||
|
'device_id': tr.data('device-id')
|
||||||
|
};
|
||||||
|
console.log('send_unlink_request called, ' + post_data);
|
||||||
|
$.ajax({
|
||||||
|
url: '{% url 'unlink_device' %}',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
beforeSend: prepareCSRFToken,
|
||||||
|
data: post_data,
|
||||||
|
success: function(data) {
|
||||||
|
$.modal.close();
|
||||||
|
tr.remove();
|
||||||
|
feedback("{% trans "Successfully unlinked." %}", 'success');
|
||||||
|
},
|
||||||
|
error: function(xhr, textStatus, errorThrown) {
|
||||||
|
$.modal.close();
|
||||||
|
var error = $.parseJSON(xhr.responseText).error;
|
||||||
|
feedback("{% trans "Failed to unlink the device." %}", 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.unlink-device').click(function() {
|
||||||
|
var op = $(this);
|
||||||
|
var tr = op.parents('tr');
|
||||||
|
var form = $('#unlink-device-confirm');
|
||||||
|
form.modal({appendTo: "#main", focus:false});
|
||||||
|
$($('.yes', form)).click(function() {
|
||||||
|
send_unlink_request(tr);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@@ -20,7 +20,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="tab {% block cur_messages %}{% endblock %}"><a href="{% url 'message_list' %}" class="msgs">{% trans "Messages" %}</a></li>
|
<li class="tab {% block cur_messages %}{% endblock %}"><a href="{% url 'message_list' %}" class="msgs">{% trans "Messages" %}</a></li>
|
||||||
<li class="tab {% block cur_clients %}{% endblock %}"><a href="{% url 'client_mgmt' %}" class="clients">{% trans "Clients" %}</a></li>
|
<li class="tab {% block cur_devices %}{% endblock %}"><a href="{% url 'devices' %}" class="clients">{% trans "Devices" %}</a></li>
|
||||||
<li class="tab {% block cur_contacts %}{% endblock %}"><a href="{% url 'contacts' %}" class="contacts">{% trans "Contacts" %}</a></li>
|
<li class="tab {% block cur_contacts %}{% endblock %}"><a href="{% url 'contacts' %}" class="contacts">{% trans "Contacts" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@@ -53,8 +53,10 @@ urlpatterns = patterns('',
|
|||||||
url(r'^home/wiki_page_edit/(?P<page_name>[^/]+)$', personal_wiki_page_edit, name='personal_wiki_page_edit'),
|
url(r'^home/wiki_page_edit/(?P<page_name>[^/]+)$', personal_wiki_page_edit, name='personal_wiki_page_edit'),
|
||||||
url(r'^home/wiki_page_delete/(?P<page_name>[^/]+)$', personal_wiki_page_delete, name='personal_wiki_page_delete'),
|
url(r'^home/wiki_page_delete/(?P<page_name>[^/]+)$', personal_wiki_page_delete, name='personal_wiki_page_delete'),
|
||||||
|
|
||||||
url(r'^home/clients/$', client_mgmt, name='client_mgmt'),
|
# url(r'^home/clients/$', client_mgmt, name='client_mgmt'),
|
||||||
url(r'^home/clients/unsync/$', client_unsync, name='client_unsync'),
|
# url(r'^home/clients/unsync/$', client_unsync, name='client_unsync'),
|
||||||
|
url(r'^devices/$', devices, name='devices'),
|
||||||
|
url(r'^home/devices/unlink/$', unlink_device, name='unlink_device'),
|
||||||
|
|
||||||
# url(r'^home/public/reply/(?P<msg_id>[\d]+)/$', innerpub_msg_reply, name='innerpub_msg_reply'),
|
# url(r'^home/public/reply/(?P<msg_id>[\d]+)/$', innerpub_msg_reply, name='innerpub_msg_reply'),
|
||||||
# url(r'^home/owner/(?P<owner_name>[^/]+)/$', ownerhome, name='ownerhome'),
|
# url(r'^home/owner/(?P<owner_name>[^/]+)/$', ownerhome, name='ownerhome'),
|
||||||
|
68
seahub/utils/devices.py
Normal file
68
seahub/utils/devices.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from seaserv import seafile_api
|
||||||
|
from seahub.api2.models import TokenV2, DESKTOP_PLATFORMS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'get_user_devices',
|
||||||
|
'do_unlink_device',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_user_devices(username):
|
||||||
|
devices = TokenV2.objects.get_user_devices(username)
|
||||||
|
|
||||||
|
peer_repos_map = get_user_synced_repo_infos(username)
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
if device['platform'] in DESKTOP_PLATFORMS:
|
||||||
|
peer_id = device['device_id']
|
||||||
|
device['synced_repos'] = peer_repos_map.get(peer_id, [])
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def get_user_synced_repo_infos(username):
|
||||||
|
'''Return a (client_ccnet_peer_id, synced_repos_on_that_client) dict'''
|
||||||
|
tokens = []
|
||||||
|
try:
|
||||||
|
tokens = seafile_api.list_repo_tokens_by_email(username)
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def sort_by_sync_time_descending(a, b):
|
||||||
|
if isinstance(a, dict):
|
||||||
|
return cmp(b['sync_time'], a['sync_time'])
|
||||||
|
else:
|
||||||
|
return cmp(b.sync_time, a.sync_time)
|
||||||
|
|
||||||
|
tokens.sort(sort_by_sync_time_descending, reverse=True)
|
||||||
|
|
||||||
|
peer_repos_map = {}
|
||||||
|
for token in tokens:
|
||||||
|
peer_id = token.peer_id
|
||||||
|
repo_id = token.repo_id
|
||||||
|
|
||||||
|
if peer_id not in peer_repos_map:
|
||||||
|
peer_repos_map[peer_id] = {}
|
||||||
|
|
||||||
|
peer_repos_map[peer_id][repo_id] = {
|
||||||
|
'repo_id': token.repo_id,
|
||||||
|
'repo_name': token.repo_name,
|
||||||
|
'sync_time': token.sync_time
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = {}
|
||||||
|
for peer_id, repos in peer_repos_map.iteritems():
|
||||||
|
ret[peer_id] = sorted(repos.values(), sort_by_sync_time_descending)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def do_unlink_device(username, platform, device_id):
|
||||||
|
if platform in DESKTOP_PLATFORMS:
|
||||||
|
# For desktop client, we also remove the sync tokens
|
||||||
|
if seafile_api.delete_repo_tokens_by_peer_id(username, device_id) < 0:
|
||||||
|
logger.warning('failed to delete_repo_tokens_by_peer_id')
|
||||||
|
raise Exception('failed to delete_repo_tokens_by_peer_id')
|
||||||
|
|
||||||
|
TokenV2.objects.delete_device_token(username, platform, device_id)
|
@@ -15,6 +15,7 @@ import datetime as dt
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
@@ -89,6 +90,7 @@ from seahub.utils.star import get_dir_starred_files
|
|||||||
from seahub.views.modules import MOD_PERSONAL_WIKI, \
|
from seahub.views.modules import MOD_PERSONAL_WIKI, \
|
||||||
enable_mod_for_user, disable_mod_for_user
|
enable_mod_for_user, disable_mod_for_user
|
||||||
from seahub.utils import HAS_OFFICE_CONVERTER
|
from seahub.utils import HAS_OFFICE_CONVERTER
|
||||||
|
from seahub.utils.devices import get_user_devices, do_unlink_device
|
||||||
|
|
||||||
if HAS_OFFICE_CONVERTER:
|
if HAS_OFFICE_CONVERTER:
|
||||||
from seahub.utils import prepare_converted_html, OFFICE_PREVIEW_MAX_SIZE, OFFICE_PREVIEW_MAX_PAGES
|
from seahub.utils import prepare_converted_html, OFFICE_PREVIEW_MAX_SIZE, OFFICE_PREVIEW_MAX_PAGES
|
||||||
@@ -1062,6 +1064,34 @@ def starred(request):
|
|||||||
return render_to_response('starred.html', {
|
return render_to_response('starred.html', {
|
||||||
"starred_files": starred_files,
|
"starred_files": starred_files,
|
||||||
}, context_instance=RequestContext(request))
|
}, context_instance=RequestContext(request))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def devices(request):
|
||||||
|
"""List user devices"""
|
||||||
|
username = request.user.username
|
||||||
|
user_devices = get_user_devices(username)
|
||||||
|
|
||||||
|
return render_to_response('devices.html', {
|
||||||
|
"devices": user_devices,
|
||||||
|
}, context_instance=RequestContext(request))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def unlink_device(request):
|
||||||
|
if not request.is_ajax():
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
content_type = 'application/json; charset=utf-8'
|
||||||
|
platform = request.POST.get('platform', '')
|
||||||
|
device_id = request.POST.get('device_id', '')
|
||||||
|
|
||||||
|
if not platform or not device_id:
|
||||||
|
return HttpResponseBadRequest(content_type=content_type)
|
||||||
|
|
||||||
|
do_unlink_device(request.user.username, platform, device_id)
|
||||||
|
|
||||||
|
return HttpResponse(json.dumps({'success': True}),
|
||||||
|
content_type=content_type)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_mods_check
|
@user_mods_check
|
||||||
|
Reference in New Issue
Block a user