mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-03-10 03:43:12 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7390fc3f4f | ||
|
|
5e003ede92 | ||
|
|
984eef92e2 | ||
|
|
eae6ca6e07 | ||
|
|
a6bfaa7c78 |
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.3 (03/10/2021)
|
||||||
|
- [**enhancement**] Enhancement: let user configure to open links in same tab instead on a new window/tab [#27](https://github.com/sissbruecker/linkding/issues/27)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.8.2 (02/10/2021)
|
## v1.8.2 (02/10/2021)
|
||||||
- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)
|
- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)
|
||||||
---
|
---
|
||||||
|
|||||||
21
bookmarks/templates/registration/password_change_done.html
Normal file
21
bookmarks/templates/registration/password_change_done.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'bookmarks/layout.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% block title %}Password changed{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="auth-page">
|
||||||
|
<div class="columns">
|
||||||
|
<section class="content-area column col-5 col-md-12">
|
||||||
|
<div class="content-area-header">
|
||||||
|
<h2>Password Changed</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-success">
|
||||||
|
Your password was changed successfully.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
55
bookmarks/templates/registration/password_change_form.html
Normal file
55
bookmarks/templates/registration/password_change_form.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends 'bookmarks/layout.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% block title %}Change Password{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="auth-page">
|
||||||
|
<div class="columns">
|
||||||
|
<section class="content-area column col-5 col-md-12">
|
||||||
|
<div class="content-area-header">
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{% url 'change_password' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group {% if form.old_password.errors %}has-error{% endif %}">
|
||||||
|
<label class="form-label" for="{{ form.old_password.id_for_label }}">Old password</label>
|
||||||
|
{{ form.old_password|add_class:'form-input'|attr:"placeholder: " }}
|
||||||
|
{% if form.old_password.errors %}
|
||||||
|
<div class="form-input-hint">
|
||||||
|
{{ form.old_password.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-group {% if form.new_password1.errors %}has-error{% endif %}">
|
||||||
|
<label class="form-label" for="{{ form.new_password1.id_for_label }}">New password</label>
|
||||||
|
{{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }}
|
||||||
|
{% if form.new_password1.errors %}
|
||||||
|
<div class="form-input-hint">
|
||||||
|
{{ form.new_password1.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-group {% if form.new_password2.errors %}has-error{% endif %}">
|
||||||
|
<label class="form-label" for="{{ form.new_password2.id_for_label }}">Confirm new password</label>
|
||||||
|
{{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }}
|
||||||
|
{% if form.new_password2.errors %}
|
||||||
|
<div class="form-input-hint">
|
||||||
|
{{ form.new_password2.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-3">
|
||||||
|
<input type="submit" value="Change Password" class="btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "bookmarks/layout.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="settings-page">
|
|
||||||
|
|
||||||
{% include 'settings/nav.html' %}
|
|
||||||
|
|
||||||
<section class="content-area">
|
|
||||||
<h2>API Token</h2>
|
|
||||||
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column col-6 col-md-12">
|
|
||||||
<input class="form-input" value="{{ api_token }}" disabled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p><strong>Please treat this token as you would any other credential.</strong> Any party with access to this
|
|
||||||
token can access and manage all your bookmarks.</p>
|
|
||||||
<p>If you think that a token was compromised you can revoke (delete) it in the <a href="{% url 'admin:authtoken_tokenproxy_changelist' %}">admin panel</a>. After deleting the token, a new one will be generated when you reload this settings page.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
{# Profile section #}
|
{# Profile section #}
|
||||||
<section class="content-area">
|
<section class="content-area">
|
||||||
<h2>Profile</h2>
|
<h2>Profile</h2>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'change_password' %}">Change password</a>
|
||||||
|
</p>
|
||||||
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
|
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
{% include 'settings/nav.html' %}
|
{% include 'settings/nav.html' %}
|
||||||
|
|
||||||
{# Integrations section #}
|
|
||||||
<section class="content-area">
|
<section class="content-area">
|
||||||
<h2>Browser Extension</h2>
|
<h2>Browser Extension</h2>
|
||||||
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The extension is available in the official extension stores for:</p>
|
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The extension is available in the official extension stores for:</p>
|
||||||
@@ -29,5 +28,19 @@
|
|||||||
class="btn btn-primary">📎 Add bookmark</a>
|
class="btn btn-primary">📎 Add bookmark</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="content-area">
|
||||||
|
<h2>REST API</h2>
|
||||||
|
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-6 col-md-12">
|
||||||
|
<input class="form-input" value="{{ api_token }}" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p><strong>Please treat this token as you would any other credential.</strong> Any party with access to this
|
||||||
|
token can access and manage all your bookmarks.</p>
|
||||||
|
<p>If you think that a token was compromised you can revoke (delete) it in the <a href="{% url 'admin:authtoken_tokenproxy_changelist' %}">admin panel</a>. After deleting the token, a new one will be generated when you reload this settings page.</p>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{% url 'bookmarks:settings.index' as index_url %}
|
{% url 'bookmarks:settings.index' as index_url %}
|
||||||
{% url 'bookmarks:settings.general' as general_url %}
|
{% url 'bookmarks:settings.general' as general_url %}
|
||||||
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
||||||
{% url 'bookmarks:settings.api' as api_url %}
|
|
||||||
|
|
||||||
<ul class="tab tab-block">
|
<ul class="tab tab-block">
|
||||||
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
|
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
|
||||||
@@ -10,9 +9,6 @@
|
|||||||
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
|
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
|
||||||
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="tab-item {% if request.get_full_path == api_url %}active{% endif %}">
|
|
||||||
<a href="{% url 'bookmarks:settings.api' %}">API</a>
|
|
||||||
</li>
|
|
||||||
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features 
 such as user management and bulk operations.">
|
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features 
 such as user management and bulk operations.">
|
||||||
<a href="{% url 'admin:index' %}" target="_blank">
|
<a href="{% url 'admin:index' %}" target="_blank">
|
||||||
<span>Admin</span>
|
<span>Admin</span>
|
||||||
|
|||||||
55
bookmarks/tests/test_password_change_view.py
Normal file
55
bookmarks/tests/test_password_change_view.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_user('testuser', 'test@example.com', 'initial_password')
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_change_password(self):
|
||||||
|
form_data = {
|
||||||
|
'old_password': 'initial_password',
|
||||||
|
'new_password1': 'new_password',
|
||||||
|
'new_password2': 'new_password',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse('change_password'), form_data)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('password_change_done'))
|
||||||
|
|
||||||
|
def test_change_password_done(self):
|
||||||
|
form_data = {
|
||||||
|
'old_password': 'initial_password',
|
||||||
|
'new_password1': 'new_password',
|
||||||
|
'new_password2': 'new_password',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse('change_password'), form_data, follow=True)
|
||||||
|
|
||||||
|
self.assertContains(response, 'Your password was changed successfully')
|
||||||
|
|
||||||
|
def test_should_return_error_for_invalid_old_password(self):
|
||||||
|
form_data = {
|
||||||
|
'old_password': 'wrong_password',
|
||||||
|
'new_password1': 'new_password',
|
||||||
|
'new_password2': 'new_password',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse('change_password'), form_data)
|
||||||
|
|
||||||
|
self.assertIn('old_password', response.context_data['form'].errors)
|
||||||
|
|
||||||
|
def test_should_return_error_for_mismatching_new_password(self):
|
||||||
|
form_data = {
|
||||||
|
'old_password': 'initial_password',
|
||||||
|
'new_password1': 'new_password',
|
||||||
|
'new_password2': 'wrong_password',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse('change_password'), form_data)
|
||||||
|
|
||||||
|
self.assertIn('new_password2', response.context_data['form'].errors)
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from rest_framework.authtoken.models import Token
|
|
||||||
|
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsApiViewTestCase(TestCase, BookmarkFactoryMixin):
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
user = self.get_or_create_test_user()
|
|
||||||
self.client.force_login(user)
|
|
||||||
|
|
||||||
def test_should_render_successfully(self):
|
|
||||||
response = self.client.get(reverse('bookmarks:settings.api'))
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_should_check_authentication(self):
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(reverse('bookmarks:settings.api'), follow=True)
|
|
||||||
|
|
||||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.api'))
|
|
||||||
|
|
||||||
def test_should_generate_api_token_if_not_exists(self):
|
|
||||||
self.assertEqual(Token.objects.count(), 0)
|
|
||||||
|
|
||||||
self.client.get(reverse('bookmarks:settings.api'))
|
|
||||||
|
|
||||||
self.assertEqual(Token.objects.count(), 1)
|
|
||||||
token = Token.objects.first()
|
|
||||||
self.assertEqual(token.user, self.user)
|
|
||||||
|
|
||||||
def test_should_not_generate_api_token_if_exists(self):
|
|
||||||
Token.objects.get_or_create(user=self.user)
|
|
||||||
self.assertEqual(Token.objects.count(), 1)
|
|
||||||
|
|
||||||
self.client.get(reverse('bookmarks:settings.api'))
|
|
||||||
|
|
||||||
self.assertEqual(Token.objects.count(), 1)
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
|
||||||
@@ -20,3 +21,20 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
|
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
|
||||||
|
|
||||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.integrations'))
|
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.integrations'))
|
||||||
|
|
||||||
|
def test_should_generate_api_token_if_not_exists(self):
|
||||||
|
self.assertEqual(Token.objects.count(), 0)
|
||||||
|
|
||||||
|
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||||
|
|
||||||
|
self.assertEqual(Token.objects.count(), 1)
|
||||||
|
token = Token.objects.first()
|
||||||
|
self.assertEqual(token.user, self.user)
|
||||||
|
|
||||||
|
def test_should_not_generate_api_token_if_exists(self):
|
||||||
|
Token.objects.get_or_create(user=self.user)
|
||||||
|
self.assertEqual(Token.objects.count(), 1)
|
||||||
|
|
||||||
|
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||||
|
|
||||||
|
self.assertEqual(Token.objects.count(), 1)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ urlpatterns = [
|
|||||||
path('settings', views.settings.general, name='settings.index'),
|
path('settings', views.settings.general, name='settings.index'),
|
||||||
path('settings/general', views.settings.general, name='settings.general'),
|
path('settings/general', views.settings.general, name='settings.general'),
|
||||||
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
|
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
|
||||||
path('settings/api', views.settings.api, name='settings.api'),
|
|
||||||
path('settings/import', views.settings.bookmark_import, name='settings.import'),
|
path('settings/import', views.settings.bookmark_import, name='settings.import'),
|
||||||
path('settings/export', views.settings.bookmark_export, name='settings.export'),
|
path('settings/export', views.settings.bookmark_export, name='settings.export'),
|
||||||
# API
|
# API
|
||||||
|
|||||||
@@ -43,15 +43,9 @@ def general(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def integrations(request):
|
def integrations(request):
|
||||||
application_url = request.build_absolute_uri("/bookmarks/new")
|
application_url = request.build_absolute_uri("/bookmarks/new")
|
||||||
|
api_token = Token.objects.get_or_create(user=request.user)[0]
|
||||||
return render(request, 'settings/integrations.html', {
|
return render(request, 'settings/integrations.html', {
|
||||||
'application_url': application_url,
|
'application_url': application_url,
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def api(request):
|
|
||||||
api_token = Token.objects.get_or_create(user=request.user)[0]
|
|
||||||
return render(request, 'settings/api.html', {
|
|
||||||
'api_token': api_token.key
|
'api_token': api_token.key
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkding",
|
"name": "linkding",
|
||||||
"version": "1.8.3",
|
"version": "1.8.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ urlpatterns = [
|
|||||||
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
|
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
|
||||||
name='login'),
|
name='login'),
|
||||||
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
||||||
|
path('change-password/', auth_views.PasswordChangeView.as_view(), name='change_password'),
|
||||||
|
path('password-change-done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
|
||||||
path('', include('bookmarks.urls')),
|
path('', include('bookmarks.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
||||||
|
|
||||||
if ALLOW_REGISTRATION:
|
if ALLOW_REGISTRATION:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.8.3
|
1.8.4
|
||||||
|
|||||||
Reference in New Issue
Block a user