FEATURE: add plain text password toggle to settings (#22)

* FEATURE: add plain text password toggle to settings

* clean up state types, lint, and add migrate

Co-authored-by: austinried <4966622+austinried@users.noreply.github.com>
This commit is contained in:
Theo Salzmann 2021-12-03 07:18:05 +01:00 committed by GitHub
parent 37214fcbdc
commit 9a6f8b86fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 101 additions and 15 deletions

View File

@ -1,9 +1,19 @@
import { GetAlbumList2Type } from '@app/subsonic/params' import { GetAlbumList2Type } from '@app/subsonic/params'
export interface Server { export type Server = (TokenPassword | PlainPassword) & {
id: string id: string
address: string address: string
username: string username: string
usePlainPassword: boolean
}
interface PlainPassword {
usePlainPassword: true
plainPassword: string
}
interface TokenPassword {
usePlainPassword: false
token: string token: string
salt: string salt: string
} }

View File

@ -11,6 +11,9 @@ import md5 from 'md5'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native' import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import SettingsSwitch from '@app/components/SettingsSwitch'
const PASSWORD_PLACEHOLDER = 'PASSWORD_PLACEHOLDER'
const ServerView: React.FC<{ const ServerView: React.FC<{
id?: string id?: string
@ -26,7 +29,12 @@ const ServerView: React.FC<{
const [address, setAddress] = useState(server?.address || '') const [address, setAddress] = useState(server?.address || '')
const [username, setUsername] = useState(server?.username || '') const [username, setUsername] = useState(server?.username || '')
const [password, setPassword] = useState(server?.token ? 'password' : '')
const [usePlainPassword, setUsePlainPassword] = useState(server?.usePlainPassword ?? false)
const [password, setPassword] = useState(
server?.usePlainPassword ? server.plainPassword || '' : server?.token ? PASSWORD_PLACEHOLDER : '',
)
const [testing, setTesting] = useState(false) const [testing, setTesting] = useState(false)
const [removing, setRemoving] = useState(false) const [removing, setRemoving] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
@ -48,11 +56,24 @@ const ServerView: React.FC<{
}, [navigation]) }, [navigation])
const createServer = useCallback<() => Server>(() => { const createServer = useCallback<() => Server>(() => {
const salt = server?.salt || uuidv4() if (usePlainPassword) {
return {
id: server?.id || uuidv4(),
usePlainPassword,
plainPassword: password,
address,
username,
}
}
let token: string let token: string
if (password === 'password' && server?.token) { let salt: string
if (server && !server.usePlainPassword && password === PASSWORD_PLACEHOLDER) {
salt = server.salt
token = server.token token = server.token
} else { } else {
salt = uuidv4()
token = md5(password + salt) token = md5(password + salt)
} }
@ -60,10 +81,11 @@ const ServerView: React.FC<{
id: server?.id || uuidv4(), id: server?.id || uuidv4(),
address, address,
username, username,
usePlainPassword,
salt, salt,
token, token,
} }
}, [address, password, server?.id, server?.salt, server?.token, username]) }, [usePlainPassword, server, address, username, password])
const save = useCallback(() => { const save = useCallback(() => {
if (!validate()) { if (!validate()) {
@ -105,6 +127,25 @@ const ServerView: React.FC<{
waitForRemove() waitForRemove()
}, [canRemove, exit, id, removeServer]) }, [canRemove, exit, id, removeServer])
const togglePlainPassword = useCallback(
(value: boolean) => {
setUsePlainPassword(value)
if (value) {
if (server && server.usePlainPassword) {
setPassword(server.plainPassword)
} else if (server) {
setPassword('')
}
} else {
if (server && !server.usePlainPassword) {
setPassword(PASSWORD_PLACEHOLDER)
}
}
},
[server],
)
const test = useCallback(() => { const test = useCallback(() => {
setTesting(true) setTesting(true)
const potential = createServer() const potential = createServer()
@ -180,6 +221,16 @@ const ServerView: React.FC<{
value={password} value={password}
onChangeText={setPassword} onChangeText={setPassword}
/> />
<SettingsSwitch
title="Force plain text password"
subtitle={
usePlainPassword
? 'Send password in plain text (legacy, make sure your connection is secure!)'
: 'Send password as token + salt'
}
value={usePlainPassword}
setValue={togglePlainPassword}
/>
<Button <Button
disabled={disableControls()} disabled={disableControls()}
style={styles.button} style={styles.button}

11
app/state/migrations.ts Normal file
View File

@ -0,0 +1,11 @@
const migrations: Array<(state: any) => any> = [
state => {
for (let server of state.settings.servers) {
server.usePlainPassword = false
}
return state
},
]
export default migrations

View File

@ -4,10 +4,13 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import create from 'zustand' import create from 'zustand'
import { persist, StateStorage } from 'zustand/middleware' import { persist, StateStorage } from 'zustand/middleware'
import { CacheSlice, createCacheSlice } from './cache' import { CacheSlice, createCacheSlice } from './cache'
import migrations from './migrations'
import { createMusicMapSlice, MusicMapSlice } from './musicmap' import { createMusicMapSlice, MusicMapSlice } from './musicmap'
import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer' import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer'
import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap' import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap'
const DB_VERSION = migrations.length
export type Store = SettingsSlice & export type Store = SettingsSlice &
MusicSlice & MusicSlice &
MusicMapSlice & MusicMapSlice &
@ -51,6 +54,7 @@ export const useStore = create<Store>(
}), }),
{ {
name: '@appStore', name: '@appStore',
version: DB_VERSION,
getStorage: () => storage, getStorage: () => storage,
whitelist: ['settings', 'cacheFiles'], whitelist: ['settings', 'cacheFiles'],
onRehydrateStorage: _preState => { onRehydrateStorage: _preState => {
@ -59,6 +63,17 @@ export const useStore = create<Store>(
postState?.setHydrated(true) postState?.setHydrated(true)
} }
}, },
migrate: (persistedState, version) => {
if (version > DB_VERSION) {
throw new Error('cannot migrate db on a downgrade, delete all data first')
}
for (let i = version; i < DB_VERSION; i++) {
persistedState = migrations[i](persistedState)
}
return persistedState
},
}, },
), ),
) )

View File

@ -64,8 +64,14 @@ export class SubsonicApiClient {
this.params = new URLSearchParams() this.params = new URLSearchParams()
this.params.append('u', server.username) this.params.append('u', server.username)
if (server.usePlainPassword) {
this.params.append('p', server.plainPassword)
} else {
this.params.append('t', server.token) this.params.append('t', server.token)
this.params.append('s', server.salt) this.params.append('s', server.salt)
}
this.params.append('v', '1.15.0') this.params.append('v', '1.15.0')
this.params.append('c', 'subtracks') this.params.append('c', 'subtracks')
} }

View File

@ -74,13 +74,6 @@
}, },
"jest": { "jest": {
"preset": "react-native", "preset": "react-native",
"moduleFileExtensions": [ "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
} }
} }