feat(mobile): identité user-first — login/password, SecureStore, scoping par compte
- api: DeviceRegistration device-only, login/changePassword/resolveUser, interception 401 (purge) exonérée sur le login ; type User aligné sur le fil (is_admin) - SecureStore (expo-secure-store v57) : token + profil, jamais en SQLite ; miroir active_user_id - AuthContext : statut loading/signedOut/signedIn, bootstrap register→restore, signIn/signOut, garde-fou switch de compte - app/login.tsx + gate de routes dans _layout (Redirect signedOut) ; i18n fr/en - DB v5 : user_id sur pending_operations + resource_permissions (UNIQUE par user), repos scopés (user_id IS ? OR IS NULL) - syncOutbox : delta permissions par compte (Map), outbox poussée du compte actif uniquement - tests: register/login/401/resolve/changePassword, scoping outbox+permissions, migrations v5 ; e2e live revert register→login admin
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { Alert, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { ApiError } from '../api/client';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const { signIn, user } = useAuth();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!username.trim() || !password) {
|
||||
setError(i18n.t('login_error_required'));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
// Changer de compte en étant connecté remplace la session locale (garde-fou).
|
||||
if (user) {
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
Alert.alert(i18n.t('login_switch_title'), i18n.t('login_switch_message'), [
|
||||
{ text: i18n.t('login_switch_cancel'), style: 'cancel', onPress: () => resolve(false) },
|
||||
{ text: i18n.t('login_switch_confirm'), style: 'destructive', onPress: () => resolve(true) },
|
||||
]);
|
||||
});
|
||||
if (!confirmed) {
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await signIn(username.trim(), password);
|
||||
router.replace('/');
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : i18n.t('login_error_generic'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{i18n.t('login_subtitle')}</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={i18n.t('login_username')}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={i18n.t('login_password')}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
/>
|
||||
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||
<Pressable
|
||||
style={[styles.button, submitting && styles.buttonDisabled]}
|
||||
onPress={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{submitting ? i18n.t('login_submitting') : i18n.t('login_submit')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
padding: 24,
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
input: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
fontSize: 15,
|
||||
},
|
||||
error: {
|
||||
color: '#c5221f',
|
||||
fontSize: 14,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#1a73e8',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user