Privileged app passwords (#4200)

* add checkbox to create privileged app password

* add indicator to privileged app pwds to list

* bump api

* oops missed the yarnlock

* adjust modal padding

* lowercase

* one more lowercase

---------

Co-authored-by: Hailey <me@haileyok.com>
zio/stable
Samuel Newman 2024-05-24 00:10:13 +01:00 committed by GitHub
parent 406993cf0e
commit d2c42cf169
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 145 additions and 110 deletions

View File

@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.12.11", "@atproto/api": "^0.12.13",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",

View File

@ -25,12 +25,13 @@ export function useAppPasswordCreateMutation() {
return useMutation< return useMutation<
ComAtprotoServerCreateAppPassword.OutputSchema, ComAtprotoServerCreateAppPassword.OutputSchema,
Error, Error,
{name: string} {name: string; privileged: boolean}
>({ >({
mutationFn: async ({name}) => { mutationFn: async ({name, privileged}) => {
return ( return (
await getAgent().com.atproto.server.createAppPassword({ await getAgent().com.atproto.server.createAppPassword({
name, name,
privileged,
}) })
).data ).data
}, },

View File

@ -8,20 +8,23 @@ import {
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {s} from '#/lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import { import {
useAppPasswordCreateMutation, useAppPasswordCreateMutation,
useAppPasswordsQuery, useAppPasswordsQuery,
} from '#/state/queries/app-passwords' } from '#/state/queries/app-passwords'
import {usePalette} from 'lib/hooks/usePalette' import {Button} from '#/view/com/util/forms/Button'
import {s} from 'lib/styles' import {Text} from '#/view/com/util/text/Text'
import {isNative} from 'platform/detection' import * as Toast from '#/view/com/util/Toast'
import {Button} from '../util/forms/Button' import {atoms as a} from '#/alf'
import {Text} from '../util/text/Text' import * as Toggle from '#/components/forms/Toggle'
import * as Toast from '../util/Toast' import {KeyboardPadding} from '#/components/KeyboardPadding'
export const snapPoints = ['70%'] export const snapPoints = ['90%']
const shadesOfBlue: string[] = [ const shadesOfBlue: string[] = [
'AliceBlue', 'AliceBlue',
@ -70,6 +73,7 @@ export function Component({}: {}) {
) )
const [appPassword, setAppPassword] = useState<string>() const [appPassword, setAppPassword] = useState<string>()
const [wasCopied, setWasCopied] = useState(false) const [wasCopied, setWasCopied] = useState(false)
const [privileged, setPrivileged] = useState(false)
const onCopy = React.useCallback(() => { const onCopy = React.useCallback(() => {
if (appPassword) { if (appPassword) {
@ -109,7 +113,7 @@ export function Component({}: {}) {
} }
try { try {
const newPassword = await mutateAppPassword({name}) const newPassword = await mutateAppPassword({name, privileged})
if (newPassword) { if (newPassword) {
setAppPassword(newPassword.password) setAppPassword(newPassword.password)
} else { } else {
@ -140,25 +144,15 @@ export function Component({}: {}) {
return ( return (
<View style={[styles.container, pal.view]} testID="addAppPasswordsModal"> <View style={[styles.container, pal.view]} testID="addAppPasswordsModal">
<View>
{!appPassword ? ( {!appPassword ? (
<>
<View>
<Text type="lg" style={[pal.text]}> <Text type="lg" style={[pal.text]}>
<Trans> <Trans>
Please enter a unique name for this App Password or use our Please enter a unique name for this App Password or use our
randomly generated one. randomly generated one.
</Trans> </Trans>
</Text> </Text>
) : (
<Text type="lg" style={[pal.text]}>
<Text type="lg-bold" style={[pal.text, s.mr5]}>
<Trans>Here is your app password.</Trans>
</Text>
<Trans>
Use this to sign into the other app along with your handle.
</Trans>
</Text>
)}
{!appPassword ? (
<View style={[pal.btn, styles.textInputWrapper]}> <View style={[pal.btn, styles.textInputWrapper]}>
<TextInput <TextInput
style={[styles.input, pal.text]} style={[styles.input, pal.text]}
@ -181,7 +175,38 @@ export function Component({}: {}) {
accessibilityHint={_(msg`Input name for app password`)} accessibilityHint={_(msg`Input name for app password`)}
/> />
</View> </View>
</View>
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
<Trans>
Can only contain letters, numbers, spaces, dashes, and
underscores. Must be at least 4 characters long, but no more than
32 characters long.
</Trans>
</Text>
<Toggle.Item
type="checkbox"
label={_(msg`Allow access to your direct messages`)}
value={privileged}
onChange={val => setPrivileged(val)}
name="privileged"
style={a.my_md}>
<Toggle.Checkbox />
<Toggle.LabelText>
<Trans>Allow access to your direct messages</Trans>
</Toggle.LabelText>
</Toggle.Item>
</>
) : ( ) : (
<>
<View>
<Text type="lg" style={[pal.text]}>
<Text type="lg-bold" style={[pal.text, s.mr5]}>
<Trans>Here is your app password.</Trans>
</Text>
<Trans>
Use this to sign into the other app along with your handle.
</Trans>
</Text>
<TouchableOpacity <TouchableOpacity
style={[pal.border, styles.passwordContainer, pal.btn]} style={[pal.border, styles.passwordContainer, pal.btn]}
onPress={onCopy} onPress={onCopy}
@ -203,23 +228,14 @@ export function Component({}: {}) {
/> />
)} )}
</TouchableOpacity> </TouchableOpacity>
)}
</View> </View>
{appPassword ? (
<Text type="lg" style={[pal.textLight, s.mb10]}> <Text type="lg" style={[pal.textLight, s.mb10]}>
<Trans> <Trans>
For security reasons, you won't be able to view this again. If you For security reasons, you won't be able to view this again. If you
lose this password, you'll need to generate a new one. lose this password, you'll need to generate a new one.
</Trans> </Trans>
</Text> </Text>
) : ( </>
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
<Trans>
Can only contain letters, numbers, spaces, dashes, and underscores.
Must be at least 4 characters long, but no more than 32 characters
long.
</Trans>
</Text>
)} )}
<View style={styles.btnContainer}> <View style={styles.btnContainer}>
<Button <Button
@ -230,6 +246,7 @@ export function Component({}: {}) {
onPress={!appPassword ? createAppPassword : onDone} onPress={!appPassword ? createAppPassword : onDone}
/> />
</View> </View>
<KeyboardPadding />
</View> </View>
) )
} }

View File

@ -5,32 +5,34 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ScrollView} from 'react-native-gesture-handler' import {ScrollView} from 'react-native-gesture-handler'
import {Text} from '../com/util/text/Text' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Button} from '../com/util/forms/Button' import {msg, Trans} from '@lingui/macro'
import * as Toast from '../com/util/Toast'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useAnalytics} from 'lib/analytics/analytics'
import {useFocusEffect} from '@react-navigation/native'
import {ViewHeader} from '../com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import { import {
useAppPasswordsQuery,
useAppPasswordDeleteMutation, useAppPasswordDeleteMutation,
useAppPasswordsQuery,
} from '#/state/queries/app-passwords' } from '#/state/queries/app-passwords'
import {ErrorScreen} from '../com/util/error/ErrorScreen' import {useSetMinimalShellMode} from '#/state/shell'
import {cleanError} from '#/lib/strings/errors' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import * as Prompt from '#/components/Prompt' import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export function AppPasswords({}: Props) { export function AppPasswords({}: Props) {
@ -135,6 +137,7 @@ export function AppPasswords({}: Props) {
testID={`appPassword-${i}`} testID={`appPassword-${i}`}
name={password.name} name={password.name}
createdAt={password.createdAt} createdAt={password.createdAt}
privileged={password.privileged}
/> />
))} ))}
{isTabletOrDesktop && ( {isTabletOrDesktop && (
@ -207,10 +210,12 @@ function AppPassword({
testID, testID,
name, name,
createdAt, createdAt,
privileged,
}: { }: {
testID: string testID: string
name: string name: string
createdAt: string createdAt: string
privileged?: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
@ -255,6 +260,18 @@ function AppPassword({
}).format(new Date(createdAt))} }).format(new Date(createdAt))}
</Trans> </Trans>
</Text> </Text>
{privileged && (
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_xs]}>
<FontAwesomeIcon
icon="circle-exclamation"
color={pal.colors.textLight}
size={14}
/>
<Text type="md" style={pal.textLight}>
Allows access to direct messages
</Text>
</View>
)}
</View> </View>
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />

View File

@ -34,10 +34,10 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@atproto/api@^0.12.11": "@atproto/api@^0.12.13":
version "0.12.11" version "0.12.13"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.11.tgz#d117a0c81395153289e99bafa760a05c2836896f" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.13.tgz#269d6c57ea894e23f20b28bd3cbfed944bd28528"
integrity sha512-NABsZ4ZYznWisr1bGuP6Z4X1GTiu5gNrmAQTxWp45M8RX88BFP1PskoG3J42d2iiyQMVBwTdoENTFYzvsKBuQg== integrity sha512-pRSID6w8AUiZJoCxgctMPRTSGVFHq7wphAnxEbRLBP3OQ1g+BRZUcqFw+e+17Pd3wrc8VImjiD4HCWtCJvCx3w==
dependencies: dependencies:
"@atproto/common-web" "^0.3.0" "@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0" "@atproto/lexicon" "^0.4.0"