Compare commits

..

5 Commits

Author SHA1 Message Date
Sascha Ißbrücker
7390fc3f4f Bump version 2021-10-16 05:44:05 +02:00
Sascha Ißbrücker
5e003ede92 Change api token field to readonly 2021-10-16 05:43:35 +02:00
Sascha Ißbrücker
984eef92e2 Add password change view (#168) 2021-10-16 05:42:04 +02:00
Sascha Ißbrücker
eae6ca6e07 Merge API view with integrations view (#165) 2021-10-03 15:13:45 +02:00
Sascha Ißbrücker
a6bfaa7c78 Update CHANGELOG.md 2021-10-03 09:54:10 +02:00
15 changed files with 177 additions and 80 deletions

View File

@@ -1,5 +1,10 @@
# 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)
- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)
---

View 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 %}

View 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 %}

View File

@@ -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 %}

View File

@@ -9,6 +9,9 @@
{# Profile section #}
<section class="content-area">
<h2>Profile</h2>
<p>
<a href="{% url 'change_password' %}">Change password</a>
</p>
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
{% csrf_token %}
<div class="form-group">

View File

@@ -5,7 +5,6 @@
{% include 'settings/nav.html' %}
{# Integrations section #}
<section class="content-area">
<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>
@@ -29,5 +28,19 @@
class="btn btn-primary">📎 Add bookmark</a>
</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>
{% endblock %}

View File

@@ -1,7 +1,6 @@
{% url 'bookmarks:settings.index' as index_url %}
{% url 'bookmarks:settings.general' as general_url %}
{% url 'bookmarks:settings.integrations' as integrations_url %}
{% url 'bookmarks:settings.api' as api_url %}
<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 %}">
@@ -10,9 +9,6 @@
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
</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 &#010; such as user management and bulk operations.">
<a href="{% url 'admin:index' %}" target="_blank">
<span>Admin</span>

View 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)

View File

@@ -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)

View File

@@ -1,5 +1,6 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.authtoken.models import Token
from bookmarks.tests.helpers import BookmarkFactoryMixin
@@ -20,3 +21,20 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
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)

View File

@@ -23,7 +23,6 @@ urlpatterns = [
path('settings', views.settings.general, name='settings.index'),
path('settings/general', views.settings.general, name='settings.general'),
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/export', views.settings.bookmark_export, name='settings.export'),
# API

View File

@@ -43,15 +43,9 @@ def general(request):
@login_required
def integrations(request):
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', {
'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
})

View File

@@ -1,6 +1,6 @@
{
"name": "linkding",
"version": "1.8.3",
"version": "1.8.4",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -25,11 +25,14 @@ urlpatterns = [
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
name='login'),
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')),
]
if DEBUG:
import debug_toolbar
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
if ALLOW_REGISTRATION:

View File

@@ -1 +1 @@
1.8.3
1.8.4