Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Georg Abenthung
funkwhale
Commits
ea6bfbc7
Commit
ea6bfbc7
authored
Jul 29, 2021
by
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
Changes
8
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
api/config/routing.py
View file @
ea6bfbc7
...
...
@@ -8,7 +8,9 @@ application = ProtocolTypeRouter(
{
# Empty for now (http->django views is added by default)
"websocket"
:
AuthMiddlewareStack
(
URLRouter
([
url
(
"^api/v1/activity$"
,
consumers
.
InstanceActivityConsumer
)])
URLRouter
(
[
url
(
"^api/v1/activity$"
,
consumers
.
InstanceActivityConsumer
.
as_asgi
())]
)
)
}
)
api/requirements/base.txt
View file @
ea6bfbc7
...
...
@@ -36,7 +36,7 @@ pymemoize~=1.0.0
django-dynamic-preferences~=1.10
python-magic~=0.4.0
channels~=
2.4.0
channels~=
3.0.3
channels_redis~=3.3.0
uvicorn[standard]~=0.14.0
gunicorn~=20.1.0
...
...
api/requirements/test.txt
View file @
ea6bfbc7
...
...
@@ -8,5 +8,6 @@ pytest-env~=0.6.0
pytest-mock~=3.6.0
pytest-randomly~=3.8.0
pytest-sugar~=0.9.0
pytest-asyncio~=0.15.1
requests-mock~=1.9.0
faker~=8.9.1
api/tests/channels/test_consumers.py
View file @
ea6bfbc7
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
):
m
=
mocker
.
patch
(
"funkwhale_api.common.consumers.JsonAuthConsumer.close"
)
scope
=
{
"user"
:
None
}
co
nsumer
=
consumers
.
JsonAuthConsumer
(
scope
=
scope
)
con
sume
r
.
connect
()
m
.
assert
_called_once_with
()
@
pytest
.
mark
.
asyncio
async
def
test_auth_consumer_requires_valid_user
():
communicator
=
WebsocketCommunicator
(
JsonAuthConsumer
.
as_asgi
(),
"api/v1/activity"
)
co
mmunicator
.
scope
[
"user"
]
=
None
con
nected
,
subprotocol
=
await
communicato
r
.
connect
()
assert
not
connected
def
test_auth_consumer_requires_user_in_scope
(
mocker
):
m
=
mocker
.
patch
(
"funkwhale_api.common.consumers.JsonAuthConsumer.close"
)
scope
=
{}
consumer
=
consumers
.
JsonAuthConsumer
(
scope
=
scope
)
consumer
.
connect
()
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
()
@
pytest
.
mark
.
asyncio
async
def
test_auth_consumer_requires_user_in_scope
():
communicator
=
WebsocketCommunicator
(
JsonAuthConsumer
.
as_asgi
(),
"api/v1/activity"
)
connected
,
subprotocol
=
await
communicator
.
connect
()
assert
not
connected
front/.eslintrc.js
View file @
ea6bfbc7
...
...
@@ -13,7 +13,8 @@ module.exports = {
},
parserOptions
:
{
ecmaVersion
:
2018
,
sourceType
:
'
module
'
sourceType
:
'
module
'
,
parser
:
'
babel-eslint
'
},
plugins
:
[
'
vue
'
...
...
front/src/router/index.js
View file @
ea6bfbc7
This diff is collapsed.
Click to expand it.
front/src/store/auth.js
View file @
ea6bfbc7
import
Vue
from
'
vue
'
import
axios
from
'
axios
'
import
logger
from
'
@/logging
'
import
router
from
'
@/router
'
import
lodash
from
'
@/lodash
'
function
getDefaultScopedTokens
()
{
return
{
listen
:
null
,
listen
:
null
}
}
function
asForm
(
obj
)
{
le
t
data
=
new
FormData
()
cons
t
data
=
new
FormData
()
Object
.
entries
(
obj
).
forEach
((
e
)
=>
{
data
.
set
(
e
[
0
],
e
[
1
])
})
return
data
}
let
baseUrl
=
`
${
window
.
location
.
protocol
}
//
${
window
.
location
.
hostname
}
`
if
(
window
.
location
.
port
)
{
baseUrl
=
`
${
baseUrl
}
:
${
window
.
location
.
port
}
`
...
...
@@ -28,14 +26,14 @@ function getDefaultOauth () {
clientId
:
null
,
clientSecret
:
null
,
accessToken
:
null
,
refreshToken
:
null
,
refreshToken
:
null
}
}
const
NEEDED_SCOPES
=
[
"
read
"
,
"
write
"
,
'
read
'
,
'
write
'
].
join
(
'
'
)
async
function
createOauthApp
(
domain
)
{
async
function
createOauthApp
(
domain
)
{
const
payload
=
{
name
:
`Funkwhale web client at
${
window
.
location
.
hostname
}
`
,
website
:
baseUrl
,
...
...
@@ -112,9 +110,9 @@ export default {
state
.
token
=
value
},
scopedTokens
:
(
state
,
value
)
=>
{
state
.
scopedTokens
=
{...
value
}
state
.
scopedTokens
=
{
...
value
}
},
permission
:
(
state
,
{
key
,
status
})
=>
{
permission
:
(
state
,
{
key
,
status
})
=>
{
state
.
availablePermissions
[
key
]
=
status
},
profilePartialUpdate
:
(
state
,
payload
)
=>
{
...
...
@@ -133,8 +131,9 @@ export default {
},
actions
:
{
// Send a request to the login URL and save the returned JWT
login
({
commit
,
dispatch
},
{
next
,
credentials
,
onError
})
{
var
form
=
new
FormData
();
login
({
commit
,
dispatch
},
{
next
,
credentials
,
onError
})
{
const
router
=
require
(
'
@/router
'
).
default
var
form
=
new
FormData
()
Object
.
keys
(
credentials
).
forEach
((
k
)
=>
{
form
.
set
(
k
,
credentials
[
k
])
})
...
...
@@ -150,13 +149,13 @@ export default {
onError
(
response
)
})
},
async
logout
({
state
,
commit
})
{
async
logout
({
state
,
commit
})
{
try
{
await
axios
.
post
(
'
users/logout
'
)
}
catch
{
console
.
log
(
'
Error while logging out, probably logged in via oauth
'
)
}
le
t
modules
=
[
cons
t
modules
=
[
'
auth
'
,
'
favorites
'
,
'
player
'
,
...
...
@@ -165,22 +164,21 @@ export default {
'
radios
'
]
modules
.
forEach
(
m
=>
{
commit
(
`
${
m
}
/reset`
,
null
,
{
root
:
true
})
commit
(
`
${
m
}
/reset`
,
null
,
{
root
:
true
})
})
logger
.
default
.
info
(
'
Log out, goodbye!
'
)
},
async
check
({
commit
,
dispatch
,
state
})
{
async
check
({
commit
,
dispatch
,
state
})
{
logger
.
default
.
info
(
'
Checking authentication…
'
)
commit
(
'
authenticated
'
,
false
)
le
t
profile
=
await
dispatch
(
'
fetchProfile
'
)
cons
t
profile
=
await
dispatch
(
'
fetchProfile
'
)
if
(
profile
)
{
commit
(
'
authenticated
'
,
true
)
}
else
{
logger
.
default
.
info
(
'
Anonymous user
'
)
}
},
fetchProfile
({
commit
,
dispatch
,
state
})
{
fetchProfile
({
commit
,
dispatch
,
state
})
{
return
new
Promise
((
resolve
,
reject
)
=>
{
axios
.
get
(
'
users/me/
'
).
then
((
response
)
=>
{
logger
.
default
.
info
(
'
Successfully fetched user profile
'
)
...
...
@@ -204,22 +202,22 @@ export default {
dispatch
(
'
playlists/fetchOwn
'
,
null
,
{
root
:
true
})
},
(
response
)
=>
{
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
)
=>
{
commit
(
"
authenticated
"
,
true
)
commit
(
"
profile
"
,
data
)
commit
(
"
username
"
,
data
.
username
)
commit
(
"
fullUsername
"
,
data
.
full_username
)
commit
(
'
authenticated
'
,
true
)
commit
(
'
profile
'
,
data
)
commit
(
'
username
'
,
data
.
username
)
commit
(
'
fullUsername
'
,
data
.
full_username
)
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
commit
(
"
permission
"
,
{
commit
(
'
permission
'
,
{
key
,
status
:
data
.
permissions
[
String
(
key
)]
})
...
...
@@ -227,45 +225,45 @@ export default {
resolve
()
})
},
async
oauthLogin
({
state
,
rootState
,
commit
,
getters
},
next
)
{
le
t
app
=
await
createOauthApp
(
getters
[
"
appDomain
"
]
)
commit
(
"
oauthApp
"
,
app
)
async
oauthLogin
({
state
,
rootState
,
commit
,
getters
},
next
)
{
cons
t
app
=
await
createOauthApp
(
getters
.
appDomain
)
commit
(
'
oauthApp
'
,
app
)
const
redirectUri
=
encodeURIComponent
(
`
${
baseUrl
}
/auth/callback`
)
le
t
params
=
`response_type=code&scope=
${
encodeURIComponent
(
NEEDED_SCOPES
)}
&redirect_uri=
${
redirectUri
}
&state=
${
next
}
&client_id=
${
state
.
oauth
.
clientId
}
`
cons
t
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
}
`
console
.
log
(
'
Redirecting user...
'
,
authorizeUrl
)
window
.
location
=
authorizeUrl
},
async
handleOauthCallback
({
state
,
commit
,
dispatch
},
authorizationCode
)
{
async
handleOauthCallback
({
state
,
commit
,
dispatch
},
authorizationCode
)
{
console
.
log
(
'
Fetching token...
'
)
const
payload
=
{
client_id
:
state
.
oauth
.
clientId
,
client_secret
:
state
.
oauth
.
clientSecret
,
grant_type
:
"
authorization_code
"
,
grant_type
:
'
authorization_code
'
,
code
:
authorizationCode
,
redirect_uri
:
`
${
baseUrl
}
/auth/callback`
}
const
response
=
await
axios
.
post
(
'
oauth/token/
'
,
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
'
)
},
async
refreshOauthToken
({
state
,
commit
},
authorizationCode
)
{
async
refreshOauthToken
({
state
,
commit
},
authorizationCode
)
{
const
payload
=
{
client_id
:
state
.
oauth
.
clientId
,
client_secret
:
state
.
oauth
.
clientSecret
,
grant_type
:
"
refresh_token
"
,
refresh_token
:
state
.
oauth
.
refreshToken
,
grant_type
:
'
refresh_token
'
,
refresh_token
:
state
.
oauth
.
refreshToken
}
le
t
response
=
await
axios
.
post
(
`
oauth/token/
`
,
cons
t
response
=
await
axios
.
post
(
'
oauth/token/
'
,
asForm
(
payload
),
{
headers
:
{
'
Content-Type
'
:
'
multipart/form-data
'
}
}
{
headers
:
{
'
Content-Type
'
:
'
multipart/form-data
'
}
}
)
commit
(
'
oauthToken
'
,
response
.
data
)
}
,
}
}
}
front/src/views/auth/Login.vue
View file @
ea6bfbc7
...
...
@@ -3,30 +3,40 @@
<section
class=
"ui vertical stripe segment"
>
<div
class=
"ui small text container"
>
<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>
</section>
</main>
</
template
>
<
script
>
import
LoginForm
from
"
@/components/auth/LoginForm
"
import
LoginForm
from
'
@/components/auth/LoginForm
'
export
default
{
props
:
{
next
:
{
type
:
String
,
default
:
"
/library
"
}
next
:
{
type
:
String
,
default
:
'
/library
'
}
},
data
()
{
return
{
redirectTo
:
this
.
next
}
},
components
:
{
LoginForm
},
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
)
{
this
.
$router
.
push
(
this
.
next
)
this
.
$router
.
push
(
this
.
redirectTo
)
}
},
computed
:
{
labels
()
{
le
t
title
=
this
.
$pgettext
(
'
Head/Login/Title
'
,
"
Log In
"
)
labels
()
{
cons
t
title
=
this
.
$pgettext
(
'
Head/Login/Title
'
,
'
Log In
'
)
return
{
title
}
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment