diff --git a/CHANGELOG b/CHANGELOG index 07d1dedbd2ba7450295491d6ce4d5cfaf1ccfb01..d010c076e34ef123fa2800eb7e541b9c8c3bd65b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,10 @@ Changelog - Front: Now reset player colors when track has no cover (#46) - Front: play button now disabled for unplayable tracks +- API: You can now enable or disable registration on the fly, via a preference (#58) +- Front: can now signup via the web interface (#35) +- Front: Fixed broken redirection on login +- Front: Fixed broken error handling on settings and login form Transcoding: diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 5fe55e53a33fdc58384d858370b4e2a5ba646cb9..491babdd15f8d4c017230f097af4b274c880a000 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -264,7 +264,7 @@ AUTHENTICATION_BACKENDS = ( ) # Some really nice defaults -ACCOUNT_AUTHENTICATION_METHOD = 'username' +ACCOUNT_AUTHENTICATION_METHOD = 'username_email' ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' @@ -317,7 +317,6 @@ CORS_ORIGIN_ALLOW_ALL = True # ) CORS_ALLOW_CREDENTIALS = True API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True) -REGISTRATION_MODE = env('REGISTRATION_MODE', default='disabled') REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', diff --git a/api/config/urls.py b/api/config/urls.py index de67ebb571de4b5f4e15cedd969d1adadeb42aee..8f7e37bc26ae56ba9967682f4ec3f19f04cc71f4 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -13,8 +13,8 @@ urlpatterns = [ url(settings.ADMIN_URL, admin.site.urls), url(r'^api/', include(("config.api_urls", 'api'), namespace="api")), - url(r'^api/auth/', include('rest_auth.urls')), - url(r'^api/auth/registration/', include('funkwhale_api.users.rest_auth_urls')), + url(r'^api/v1/auth/', include('rest_auth.urls')), + url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')), url(r'^accounts/', include('allauth.urls')), # Your stuff: custom urls includes go here diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py index 792b4860fc240439f72c8cd7dc2fe319ac633fa1..96d1b8b1d6b4aaa708bc3c09490beae84c10fa1a 100644 --- a/api/funkwhale_api/users/adapters.py +++ b/api/funkwhale_api/users/adapters.py @@ -1,15 +1,10 @@ from allauth.account.adapter import DefaultAccountAdapter -from django.conf import settings +from dynamic_preferences.registries import global_preferences_registry class FunkwhaleAccountAdapter(DefaultAccountAdapter): def is_open_for_signup(self, request): - - if settings.REGISTRATION_MODE == "disabled": - return False - if settings.REGISTRATION_MODE == "public": - return True - - return False + manager = global_preferences_registry.manager() + return manager['users__registration_enabled'] diff --git a/api/funkwhale_api/users/dynamic_preferences_registry.py b/api/funkwhale_api/users/dynamic_preferences_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..16d79da143cb3139f9a92137044b6092288e1b40 --- /dev/null +++ b/api/funkwhale_api/users/dynamic_preferences_registry.py @@ -0,0 +1,15 @@ +from dynamic_preferences import types +from dynamic_preferences.registries import global_preferences_registry + +users = types.Section('users') + + +@global_preferences_registry.register +class RegistrationEnabled(types.BooleanPreference): + show_in_api = True + section = users + name = 'registration_enabled' + default = False + verbose_name = ( + 'Can visitors open a new account on this instance?' + ) diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 1eb8ef222a79d68f41d40c8555c0c7cb9d931680..569acbd15ee5138150dd7de4112cd7ebd2d5523a 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -6,7 +6,7 @@ from django.urls import reverse from funkwhale_api.users.models import User -def test_can_create_user_via_api(settings, client, db): +def test_can_create_user_via_api(preferences, client, db): url = reverse('rest_register') data = { 'username': 'test1', @@ -14,7 +14,7 @@ def test_can_create_user_via_api(settings, client, db): 'password1': 'testtest', 'password2': 'testtest', } - settings.REGISTRATION_MODE = "public" + preferences['users__registration_enabled'] = True response = client.post(url, data) assert response.status_code == 201 @@ -22,7 +22,7 @@ def test_can_create_user_via_api(settings, client, db): assert u.username == 'test1' -def test_can_disable_registration_view(settings, client, db): +def test_can_disable_registration_view(preferences, client, db): url = reverse('rest_register') data = { 'username': 'test1', @@ -30,7 +30,7 @@ def test_can_disable_registration_view(settings, client, db): 'password1': 'testtest', 'password2': 'testtest', } - settings.REGISTRATION_MODE = "disabled" + preferences['users__registration_enabled'] = False response = client.post(url, data) assert response.status_code == 403 diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 5bdfeb9c626fa074eaedb5c7f499871946d3dea9..6a4b15b67cf04857b87296c30a23060e650f2d7c 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -74,11 +74,6 @@ DJANGO_SECRET_KEY= # If True, unauthenticated users won't be able to query the API API_AUTHENTICATION_REQUIRED=True -# What is the workflow for registration on funkwhale ? Possible values: -# public: anybody can register an account -# disabled: nobody can register an account -REGISTRATION_MODE=disabled - # Sentry/Raven error reporting (server side) # Enable Raven if you want to help improve funkwhale by # automatically sending error reports our Sentry instance. diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue index 99b439af8b3e25c821ddb1dbc0c86c6419961d2c..2cf6d5f6db1cd8bc4e4d72d96be5c70d559e9452 100644 --- a/front/src/components/auth/Login.vue +++ b/front/src/components/auth/Login.vue @@ -12,13 +12,13 @@ </ul> </div> <div class="field"> - <label>Username</label> + <label>Username or email</label> <input ref="username" required type="text" autofocus - placeholder="Enter your username" + placeholder="Enter your username or email" v-model="credentials.username" > </div> @@ -32,6 +32,9 @@ > </div> <button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Login</button> + <router-link class="ui right floated basic button" :to="{path: '/signup'}"> + Create an account + </router-link> </form> </div> </div> @@ -73,9 +76,9 @@ export default { // to properly make use of http in the auth service this.$store.dispatch('auth/login', { credentials, - next: this.next, - onError: response => { - if (response.status === 400) { + next: '/library', + onError: error => { + if (error.response.status === 400) { self.error = 'invalid_credentials' } else { self.error = 'unknown_error' diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue index f090581ef7305927e293e3f6ffce3e6033131004..4e8f33289b470526539e71ff40f6de32a2857dcb 100644 --- a/front/src/components/auth/Settings.vue +++ b/front/src/components/auth/Settings.vue @@ -37,7 +37,6 @@ <script> import axios from 'axios' -import config from '@/config' import logger from '@/logging' export default { @@ -61,12 +60,16 @@ export default { new_password1: this.new_password, new_password2: this.new_password } - let url = config.BACKEND_URL + 'api/auth/registration/change-password/' + let url = 'auth/registration/change-password/' return axios.post(url, credentials).then(response => { logger.default.info('Password successfully changed') - self.$router.push('/profile/me') - }, response => { - if (response.status === 400) { + self.$router.push({ + name: 'profile', + params: { + username: self.$store.state.auth.username + }}) + }, error => { + if (error.response.status === 400) { self.error = 'invalid_credentials' } else { self.error = 'unknown_error' diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue new file mode 100644 index 0000000000000000000000000000000000000000..13b723d201437933d9f6fd46bcaccfb2d316f5ed --- /dev/null +++ b/front/src/components/auth/Signup.vue @@ -0,0 +1,137 @@ +<template> + <div class="main pusher"> + <div class="ui vertical stripe segment"> + <div class="ui small text container"> + <h2>Create a funkwhale account</h2> + <form + v-if="$store.state.instance.settings.users.registration_enabled.value" + :class="['ui', {'loading': isLoadingInstanceSetting}, 'form']" + @submit.prevent="submit()"> + <div v-if="errors.length > 0" class="ui negative message"> + <div class="header">We cannot create your account</div> + <ul class="list"> + <li v-for="error in errors">{{ error }}</li> + </ul> + </div> + <div class="field"> + <label>Username</label> + <input + ref="username" + required + type="text" + autofocus + placeholder="Enter your username" + v-model="username"> + </div> + <div class="field"> + <label>Email</label> + <input + ref="email" + required + type="email" + placeholder="Enter your email" + v-model="email"> + </div> + <div class="field"> + <label>Password</label> + <div class="ui action input"> + <input + required + :type="passwordInputType" + placeholder="Enter your password" + v-model="password"> + <span @click="showPassword = !showPassword" title="Show/hide password" class="ui icon button"> + <i class="eye icon"></i> + </span> + </div> + </div> + <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit">Create my account</button> + </form> + <p v-else>Registration is currently disabled on this instance, please try again later.</p> + </div> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import logger from '@/logging' + +export default { + name: 'login', + props: { + next: {type: String, default: '/'} + }, + data () { + return { + username: '', + email: '', + password: '', + isLoadingInstanceSetting: true, + errors: [], + isLoading: false, + showPassword: false + } + }, + created () { + let self = this + this.$store.dispatch('instance/fetchSettings', { + callback: function () { + self.isLoadingInstanceSetting = false + } + }) + }, + methods: { + submit () { + var self = this + self.isLoading = true + this.errors = [] + var payload = { + username: this.username, + password1: this.password, + password2: this.password, + email: this.email + } + return axios.post('auth/registration/', payload).then(response => { + logger.default.info('Successfully created account') + self.$router.push({ + name: 'profile', + params: { + username: this.username + }}) + }, error => { + self.errors = this.getErrors(error.response) + self.isLoading = false + }) + }, + getErrors (response) { + let errors = [] + if (response.status !== 400) { + errors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running') + return errors + } + for (var field in response.data) { + if (response.data.hasOwnProperty(field)) { + response.data[field].forEach(e => { + errors.push(e) + }) + } + } + return errors + } + }, + computed: { + passwordInputType () { + if (this.showPassword) { + return 'text' + } + return 'password' + } + } + +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> diff --git a/front/src/router/index.js b/front/src/router/index.js index ea8854bbe48e4f06ec4369a2853b3e98271df733..c1d03e059442ff98731bb903159d532b62ef4286 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -3,6 +3,7 @@ import Router from 'vue-router' import PageNotFound from '@/components/PageNotFound' import Home from '@/components/Home' import Login from '@/components/auth/Login' +import Signup from '@/components/auth/Signup' import Profile from '@/components/auth/Profile' import Settings from '@/components/auth/Settings' import Logout from '@/components/auth/Logout' @@ -38,6 +39,11 @@ export default new Router({ component: Login, props: (route) => ({ next: route.query.next || '/library' }) }, + { + path: '/signup', + name: 'signup', + component: Signup + }, { path: '/logout', name: 'logout', diff --git a/front/src/store/instance.js b/front/src/store/instance.js index a0071f0961d6536f2133331787d90245a6dc1df4..80003db0dce3e1d27ff3de814a8149826dafd6db 100644 --- a/front/src/store/instance.js +++ b/front/src/store/instance.js @@ -6,6 +6,11 @@ export default { namespaced: true, state: { settings: { + users: { + registration_enabled: { + value: true + } + }, raven: { front_enabled: { value: false @@ -23,7 +28,7 @@ export default { }, actions: { // Send a request to the login URL and save the returned JWT - fetchSettings ({commit}) { + fetchSettings ({commit}, payload) { return axios.get('instance/settings/').then(response => { logger.default.info('Successfully fetched instance settings') let sections = {} @@ -34,6 +39,9 @@ export default { sections[e.section][e.name] = e }) commit('settings', sections) + if (payload && payload.callback) { + callback() + } }, response => { logger.default.error('Error while fetching settings', response.data) })