Commit ea6bfbc7 authored by Georg Abenthung's avatar Georg Abenthung
Browse files

Merge remote-tracking branch 'upstream/develop' into develop

parents 04dd1067 1a362a24
Pipeline #15450 passed with stages
in 15 minutes and 20 seconds
...@@ -8,7 +8,9 @@ application = ProtocolTypeRouter( ...@@ -8,7 +8,9 @@ application = ProtocolTypeRouter(
{ {
# Empty for now (http->django views is added by default) # Empty for now (http->django views is added by default)
"websocket": AuthMiddlewareStack( "websocket": AuthMiddlewareStack(
URLRouter([url("^api/v1/activity$", consumers.InstanceActivityConsumer)]) URLRouter(
[url("^api/v1/activity$", consumers.InstanceActivityConsumer.as_asgi())]
)
) )
} }
) )
...@@ -36,7 +36,7 @@ pymemoize~=1.0.0 ...@@ -36,7 +36,7 @@ pymemoize~=1.0.0
django-dynamic-preferences~=1.10 django-dynamic-preferences~=1.10
python-magic~=0.4.0 python-magic~=0.4.0
channels~=2.4.0 channels~=3.0.3
channels_redis~=3.3.0 channels_redis~=3.3.0
uvicorn[standard]~=0.14.0 uvicorn[standard]~=0.14.0
gunicorn~=20.1.0 gunicorn~=20.1.0
......
...@@ -8,5 +8,6 @@ pytest-env~=0.6.0 ...@@ -8,5 +8,6 @@ pytest-env~=0.6.0
pytest-mock~=3.6.0 pytest-mock~=3.6.0
pytest-randomly~=3.8.0 pytest-randomly~=3.8.0
pytest-sugar~=0.9.0 pytest-sugar~=0.9.0
pytest-asyncio~=0.15.1
requests-mock~=1.9.0 requests-mock~=1.9.0
faker~=8.9.1 faker~=8.9.1
from funkwhale_api.common import consumers import pytest
from channels.testing import WebsocketCommunicator
from funkwhale_api.common.consumers import JsonAuthConsumer
def test_auth_consumer_requires_valid_user(mocker): @pytest.mark.asyncio
m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.close") async def test_auth_consumer_requires_valid_user():
scope = {"user": None} communicator = WebsocketCommunicator(JsonAuthConsumer.as_asgi(), "api/v1/activity")
consumer = consumers.JsonAuthConsumer(scope=scope) communicator.scope["user"] = None
consumer.connect() connected, subprotocol = await communicator.connect()
m.assert_called_once_with() assert not connected
def test_auth_consumer_requires_user_in_scope(mocker): @pytest.mark.asyncio
m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.close") async def test_auth_consumer_requires_user_in_scope():
scope = {} communicator = WebsocketCommunicator(JsonAuthConsumer.as_asgi(), "api/v1/activity")
consumer = consumers.JsonAuthConsumer(scope=scope) connected, subprotocol = await communicator.connect()
consumer.connect() assert not connected
m.assert_called_once_with()
def test_auth_consumer_accepts_connection(mocker, factories):
user = factories["users.User"]()
m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.accept")
scope = {"user": user}
consumer = consumers.JsonAuthConsumer(scope=scope)
consumer.connect()
m.assert_called_once_with()
...@@ -13,7 +13,8 @@ module.exports = { ...@@ -13,7 +13,8 @@ module.exports = {
}, },
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: 'module' sourceType: 'module',
parser: 'babel-eslint'
}, },
plugins: [ plugins: [
'vue' 'vue'
......
This diff is collapsed.
import Vue from 'vue' import Vue from 'vue'
import axios from 'axios' import axios from 'axios'
import logger from '@/logging' import logger from '@/logging'
import router from '@/router'
import lodash from '@/lodash' import lodash from '@/lodash'
function getDefaultScopedTokens () { function getDefaultScopedTokens () {
return { return {
listen: null, listen: null
} }
} }
function asForm (obj) { function asForm (obj) {
let data = new FormData() const data = new FormData()
Object.entries(obj).forEach((e) => { Object.entries(obj).forEach((e) => {
data.set(e[0], e[1]) data.set(e[0], e[1])
}) })
return data return data
} }
let baseUrl = `${window.location.protocol}//${window.location.hostname}` let baseUrl = `${window.location.protocol}//${window.location.hostname}`
if (window.location.port) { if (window.location.port) {
baseUrl = `${baseUrl}:${window.location.port}` baseUrl = `${baseUrl}:${window.location.port}`
...@@ -28,14 +26,14 @@ function getDefaultOauth () { ...@@ -28,14 +26,14 @@ function getDefaultOauth () {
clientId: null, clientId: null,
clientSecret: null, clientSecret: null,
accessToken: null, accessToken: null,
refreshToken: null, refreshToken: null
} }
} }
const NEEDED_SCOPES = [ const NEEDED_SCOPES = [
"read", 'read',
"write", 'write'
].join(' ') ].join(' ')
async function createOauthApp(domain) { async function createOauthApp (domain) {
const payload = { const payload = {
name: `Funkwhale web client at ${window.location.hostname}`, name: `Funkwhale web client at ${window.location.hostname}`,
website: baseUrl, website: baseUrl,
...@@ -112,9 +110,9 @@ export default { ...@@ -112,9 +110,9 @@ export default {
state.token = value state.token = value
}, },
scopedTokens: (state, value) => { scopedTokens: (state, value) => {
state.scopedTokens = {...value} state.scopedTokens = { ...value }
}, },
permission: (state, {key, status}) => { permission: (state, { key, status }) => {
state.availablePermissions[key] = status state.availablePermissions[key] = status
}, },
profilePartialUpdate: (state, payload) => { profilePartialUpdate: (state, payload) => {
...@@ -133,8 +131,9 @@ export default { ...@@ -133,8 +131,9 @@ export default {
}, },
actions: { actions: {
// Send a request to the login URL and save the returned JWT // Send a request to the login URL and save the returned JWT
login ({commit, dispatch}, {next, credentials, onError}) { login ({ commit, dispatch }, { next, credentials, onError }) {
var form = new FormData(); const router = require('@/router').default
var form = new FormData()
Object.keys(credentials).forEach((k) => { Object.keys(credentials).forEach((k) => {
form.set(k, credentials[k]) form.set(k, credentials[k])
}) })
...@@ -150,13 +149,13 @@ export default { ...@@ -150,13 +149,13 @@ export default {
onError(response) onError(response)
}) })
}, },
async logout ({state, commit}) { async logout ({ state, commit }) {
try { try {
await axios.post('users/logout') await axios.post('users/logout')
} catch { } catch {
console.log('Error while logging out, probably logged in via oauth') console.log('Error while logging out, probably logged in via oauth')
} }
let modules = [ const modules = [
'auth', 'auth',
'favorites', 'favorites',
'player', 'player',
...@@ -165,22 +164,21 @@ export default { ...@@ -165,22 +164,21 @@ export default {
'radios' 'radios'
] ]
modules.forEach(m => { modules.forEach(m => {
commit(`${m}/reset`, null, {root: true}) commit(`${m}/reset`, null, { root: true })
}) })
logger.default.info('Log out, goodbye!') logger.default.info('Log out, goodbye!')
}, },
async check ({commit, dispatch, state}) { async check ({ commit, dispatch, state }) {
logger.default.info('Checking authentication…') logger.default.info('Checking authentication…')
commit('authenticated', false) commit('authenticated', false)
let profile = await dispatch('fetchProfile') const profile = await dispatch('fetchProfile')
if (profile) { if (profile) {
commit('authenticated', true) commit('authenticated', true)
} else { } else {
logger.default.info('Anonymous user') logger.default.info('Anonymous user')
} }
}, },
fetchProfile ({commit, dispatch, state}) { fetchProfile ({ commit, dispatch, state }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get('users/me/').then((response) => { axios.get('users/me/').then((response) => {
logger.default.info('Successfully fetched user profile') logger.default.info('Successfully fetched user profile')
...@@ -204,22 +202,22 @@ export default { ...@@ -204,22 +202,22 @@ export default {
dispatch('playlists/fetchOwn', null, { root: true }) dispatch('playlists/fetchOwn', null, { root: true })
}, (response) => { }, (response) => {
logger.default.info('Error while fetching user profile') logger.default.info('Error while fetching user profile')
reject() reject(new Error('Error while fetching user profile'))
}) })
}) })
}, },
updateProfile({ commit }, data) { updateProfile ({ commit }, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
commit("authenticated", true) commit('authenticated', true)
commit("profile", data) commit('profile', data)
commit("username", data.username) commit('username', data.username)
commit("fullUsername", data.full_username) commit('fullUsername', data.full_username)
if (data.tokens) { if (data.tokens) {
commit("scopedTokens", data.tokens) commit('scopedTokens', data.tokens)
} }
Object.keys(data.permissions).forEach(function(key) { Object.keys(data.permissions).forEach(function (key) {
// this makes it easier to check for permissions in templates // this makes it easier to check for permissions in templates
commit("permission", { commit('permission', {
key, key,
status: data.permissions[String(key)] status: data.permissions[String(key)]
}) })
...@@ -227,45 +225,45 @@ export default { ...@@ -227,45 +225,45 @@ export default {
resolve() resolve()
}) })
}, },
async oauthLogin({ state, rootState, commit, getters }, next) { async oauthLogin ({ state, rootState, commit, getters }, next) {
let app = await createOauthApp(getters["appDomain"]) const app = await createOauthApp(getters.appDomain)
commit("oauthApp", app) commit('oauthApp', app)
const redirectUri = encodeURIComponent(`${baseUrl}/auth/callback`) const redirectUri = encodeURIComponent(`${baseUrl}/auth/callback`)
let params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUri}&state=${next}&client_id=${state.oauth.clientId}` const params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUri}&state=${next}&client_id=${state.oauth.clientId}`
const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}` const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`
console.log('Redirecting user...', authorizeUrl) console.log('Redirecting user...', authorizeUrl)
window.location = authorizeUrl window.location = authorizeUrl
}, },
async handleOauthCallback({ state, commit, dispatch }, authorizationCode) { async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
console.log('Fetching token...') console.log('Fetching token...')
const payload = { const payload = {
client_id: state.oauth.clientId, client_id: state.oauth.clientId,
client_secret: state.oauth.clientSecret, client_secret: state.oauth.clientSecret,
grant_type: "authorization_code", grant_type: 'authorization_code',
code: authorizationCode, code: authorizationCode,
redirect_uri: `${baseUrl}/auth/callback` redirect_uri: `${baseUrl}/auth/callback`
} }
const response = await axios.post( const response = await axios.post(
'oauth/token/', 'oauth/token/',
asForm(payload), asForm(payload),
{headers: {'Content-Type': 'multipart/form-data'}} { headers: { 'Content-Type': 'multipart/form-data' } }
) )
commit("oauthToken", response.data) commit('oauthToken', response.data)
await dispatch('fetchProfile') await dispatch('fetchProfile')
}, },
async refreshOauthToken({ state, commit }, authorizationCode) { async refreshOauthToken ({ state, commit }, authorizationCode) {
const payload = { const payload = {
client_id: state.oauth.clientId, client_id: state.oauth.clientId,
client_secret: state.oauth.clientSecret, client_secret: state.oauth.clientSecret,
grant_type: "refresh_token", grant_type: 'refresh_token',
refresh_token: state.oauth.refreshToken, refresh_token: state.oauth.refreshToken
} }
let response = await axios.post( const response = await axios.post(
`oauth/token/`, 'oauth/token/',
asForm(payload), asForm(payload),
{headers: {'Content-Type': 'multipart/form-data'}} { headers: { 'Content-Type': 'multipart/form-data' } }
) )
commit('oauthToken', response.data) commit('oauthToken', response.data)
}, }
} }
} }
...@@ -3,30 +3,40 @@ ...@@ -3,30 +3,40 @@
<section class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2><translate translate-context="Content/Login/Title/Verb">Log in to your Funkwhale account</translate></h2> <h2><translate translate-context="Content/Login/Title/Verb">Log in to your Funkwhale account</translate></h2>
<login-form :next="next"></login-form> <login-form :next="redirectTo"></login-form>
</div> </div>
</section> </section>
</main> </main>
</template> </template>
<script> <script>
import LoginForm from "@/components/auth/LoginForm" import LoginForm from '@/components/auth/LoginForm'
export default { export default {
props: { props: {
next: { type: String, default: "/library" } next: { type: String, default: '/library' }
},
data () {
return {
redirectTo: this.next
}
}, },
components: { components: {
LoginForm LoginForm
}, },
created () { created () {
const resolved = this.$router.resolve(this.redirectTo)
console.log(resolved.route.name)
if (resolved.route.name === '404') {
this.redirectTo = '/library'
}
if (this.$store.state.auth.authenticated) { if (this.$store.state.auth.authenticated) {
this.$router.push(this.next) this.$router.push(this.redirectTo)
} }
}, },
computed: { computed: {
labels() { labels () {
let title = this.$pgettext('Head/Login/Title', "Log In") const title = this.$pgettext('Head/Login/Title', 'Log In')
return { return {
title title
} }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment