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'
export interface Server {
export type Server = (TokenPassword | PlainPassword) & {
id: string
address: string
username: string
usePlainPassword: boolean
}
interface PlainPassword {
usePlainPassword: true
plainPassword: string
}
interface TokenPassword {
usePlainPassword: false
token: string
salt: string
}

View File

@ -11,6 +11,9 @@ import md5 from 'md5'
import React, { useCallback, useState } from 'react'
import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native'
import { v4 as uuidv4 } from 'uuid'
import SettingsSwitch from '@app/components/SettingsSwitch'
const PASSWORD_PLACEHOLDER = 'PASSWORD_PLACEHOLDER'
const ServerView: React.FC<{
id?: string
@ -26,7 +29,12 @@ const ServerView: React.FC<{
const [address, setAddress] = useState(server?.address || '')
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 [removing, setRemoving] = useState(false)
const [saving, setSaving] = useState(false)
@ -48,11 +56,24 @@ const ServerView: React.FC<{
}, [navigation])
const createServer = useCallback<() => Server>(() => {
const salt = server?.salt || uuidv4()
if (usePlainPassword) {
return {
id: server?.id || uuidv4(),
usePlainPassword,
plainPassword: password,
address,
username,
}
}
let token: string
if (password === 'password' && server?.token) {
let salt: string
if (server && !server.usePlainPassword && password === PASSWORD_PLACEHOLDER) {
salt = server.salt
token = server.token
} else {
salt = uuidv4()
token = md5(password + salt)
}
@ -60,10 +81,11 @@ const ServerView: React.FC<{
id: server?.id || uuidv4(),
address,
username,
usePlainPassword,
salt,
token,
}
}, [address, password, server?.id, server?.salt, server?.token, username])
}, [usePlainPassword, server, address, username, password])
const save = useCallback(() => {
if (!validate()) {
@ -105,6 +127,25 @@ const ServerView: React.FC<{
waitForRemove()
}, [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(() => {
setTesting(true)
const potential = createServer()
@ -180,6 +221,16 @@ const ServerView: React.FC<{
value={password}
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
disabled={disableControls()}
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 { persist, StateStorage } from 'zustand/middleware'
import { CacheSlice, createCacheSlice } from './cache'
import migrations from './migrations'
import { createMusicMapSlice, MusicMapSlice } from './musicmap'
import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer'
import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap'
const DB_VERSION = migrations.length
export type Store = SettingsSlice &
MusicSlice &
MusicMapSlice &
@ -51,6 +54,7 @@ export const useStore = create<Store>(
}),
{
name: '@appStore',
version: DB_VERSION,
getStorage: () => storage,
whitelist: ['settings', 'cacheFiles'],
onRehydrateStorage: _preState => {
@ -59,6 +63,17 @@ export const useStore = create<Store>(
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.append('u', server.username)
this.params.append('t', server.token)
this.params.append('s', server.salt)
if (server.usePlainPassword) {
this.params.append('p', server.plainPassword)
} else {
this.params.append('t', server.token)
this.params.append('s', server.salt)
}
this.params.append('v', '1.15.0')
this.params.append('c', 'subtracks')
}

View File

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