591 lines
15 KiB
TypeScript
591 lines
15 KiB
TypeScript
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import * as LocalAuthentication from "expo-local-authentication";
|
|
import * as Location from "expo-location";
|
|
import { useRouter } from "expo-router";
|
|
import * as SQLite from "expo-sqlite";
|
|
import React, { useEffect, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
SafeAreaView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
|
|
// Types
|
|
interface User {
|
|
id: number;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
residence: string;
|
|
workplace: string;
|
|
latitude: string;
|
|
longitude: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
// Simple Auth Component
|
|
export default function AuthScreen() {
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
const [authMode, setAuthMode] = useState<"login" | "register">("login");
|
|
|
|
// Form states
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [firstName, setFirstName] = useState("");
|
|
const [lastName, setLastName] = useState("");
|
|
const [residence, setResidence] = useState("");
|
|
const [workplace, setWorkplace] = useState("");
|
|
const [latitude, setLatitude] = useState<string | null>(null);
|
|
const [longitude, setLongitude] = useState<string | null>(null);
|
|
const [biometricSupported, setBiometricSupported] = useState(false);
|
|
const [showCalendar, setShowCalendar] = useState(false);
|
|
|
|
// Database
|
|
const [db, setDb] = useState<SQLite.SQLiteDatabase | null>(null);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
initApp();
|
|
}, []);
|
|
|
|
const initApp = async () => {
|
|
try {
|
|
// Initialize database
|
|
const database = await SQLite.openDatabaseAsync("AuthApp.db");
|
|
setDb(database);
|
|
|
|
await database.execAsync(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT UNIQUE NOT NULL,
|
|
firstName TEXT NOT NULL,
|
|
lastName TEXT NOT NULL,
|
|
password TEXT NOT NULL,
|
|
residence TEXT,
|
|
workplace TEXT,
|
|
latitude TEXT,
|
|
longitude TEXT,
|
|
createdAt TEXT NOT NULL
|
|
)
|
|
`);
|
|
|
|
// Check biometric support
|
|
const compatible = await LocalAuthentication.hasHardwareAsync();
|
|
const enrolled = await LocalAuthentication.isEnrolledAsync();
|
|
setBiometricSupported(compatible && enrolled);
|
|
|
|
// Check if user is logged in
|
|
const session = await AsyncStorage.getItem("@user_session");
|
|
if (session) {
|
|
const userData = JSON.parse(session);
|
|
setCurrentUser(userData.user);
|
|
setIsLoggedIn(true);
|
|
}
|
|
} catch (error) {
|
|
console.error("App initialization failed:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRegister = async () => {
|
|
if (
|
|
!email ||
|
|
!password ||
|
|
!firstName ||
|
|
!lastName ||
|
|
!residence ||
|
|
!workplace
|
|
) {
|
|
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
Alert.alert(
|
|
"Fehler",
|
|
"Das Passwort muss mindestens 6 Zeichen lang sein."
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!db) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
const createdAt = new Date().toISOString();
|
|
const result = await db.runAsync(
|
|
"INSERT INTO users (email, firstName, lastName, password, residence, workplace, latitude, longitude, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[
|
|
email,
|
|
firstName,
|
|
lastName,
|
|
password,
|
|
residence,
|
|
workplace,
|
|
latitude ?? "",
|
|
longitude ?? "",
|
|
createdAt,
|
|
]
|
|
);
|
|
|
|
const newUser: User = {
|
|
id: result.lastInsertRowId,
|
|
email,
|
|
firstName,
|
|
lastName,
|
|
residence,
|
|
workplace,
|
|
latitude: latitude ?? "",
|
|
longitude: longitude ?? "",
|
|
createdAt,
|
|
};
|
|
|
|
// Save session
|
|
await AsyncStorage.setItem(
|
|
"@user_session",
|
|
JSON.stringify({ user: newUser })
|
|
);
|
|
|
|
setCurrentUser(newUser);
|
|
setIsLoggedIn(true);
|
|
Alert.alert("Erfolg", "Registrierung erfolgreich!");
|
|
} catch (error: any) {
|
|
if (error.message?.includes("UNIQUE constraint failed")) {
|
|
Alert.alert(
|
|
"Fehler",
|
|
"Ein Benutzer mit dieser E-Mail existiert bereits."
|
|
);
|
|
} else {
|
|
Alert.alert("Fehler", "Registrierung fehlgeschlagen.");
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogin = async () => {
|
|
if (!email || !password) {
|
|
Alert.alert("Fehler", "Bitte füllen Sie alle Felder aus.");
|
|
return;
|
|
}
|
|
|
|
if (!db) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
const result = (await db.getFirstAsync(
|
|
"SELECT * FROM users WHERE email = ? AND password = ?",
|
|
[email, password]
|
|
)) as any;
|
|
|
|
if (result) {
|
|
const user: User = {
|
|
id: result.id,
|
|
email: result.email,
|
|
firstName: result.firstName,
|
|
lastName: result.lastName,
|
|
residence: result.residence,
|
|
workplace: result.workplace,
|
|
latitude: result.latitude,
|
|
longitude: result.longitude,
|
|
createdAt: result.createdAt,
|
|
};
|
|
|
|
// Save session
|
|
await AsyncStorage.setItem("@user_session", JSON.stringify({ user }));
|
|
|
|
setCurrentUser(user);
|
|
setIsLoggedIn(true);
|
|
Alert.alert("Erfolg", "Anmeldung erfolgreich!");
|
|
} else {
|
|
Alert.alert("Fehler", "Ungültige E-Mail oder Passwort.");
|
|
}
|
|
} catch (error) {
|
|
Alert.alert("Fehler", "Anmeldung fehlgeschlagen.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBiometricAuth = async () => {
|
|
try {
|
|
const result = await LocalAuthentication.authenticateAsync({
|
|
promptMessage: "Biometrische Authentifizierung",
|
|
cancelLabel: "Abbrechen",
|
|
fallbackLabel: "Passcode verwenden",
|
|
});
|
|
|
|
if (result.success) {
|
|
// For demo, auto-login first user
|
|
if (!db) return;
|
|
|
|
const user = (await db.getFirstAsync(
|
|
"SELECT * FROM users LIMIT 1"
|
|
)) as any;
|
|
if (user) {
|
|
const userData: User = {
|
|
id: user.id,
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
residence: user.residence,
|
|
workplace: user.workplace,
|
|
latitude: user.latitude,
|
|
longitude: user.longitude,
|
|
createdAt: user.createdAt,
|
|
};
|
|
|
|
await AsyncStorage.setItem(
|
|
"@user_session",
|
|
JSON.stringify({ user: userData })
|
|
);
|
|
setCurrentUser(userData);
|
|
setIsLoggedIn(true);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
Alert.alert("Fehler", "Biometrische Authentifizierung fehlgeschlagen.");
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await AsyncStorage.removeItem("@user_session");
|
|
setCurrentUser(null);
|
|
setIsLoggedIn(false);
|
|
setEmail("");
|
|
setPassword("");
|
|
setFirstName("");
|
|
setLastName("");
|
|
setResidence("");
|
|
setWorkplace("");
|
|
setLatitude(null);
|
|
setLongitude(null);
|
|
} catch (error) {
|
|
Alert.alert("Fehler", "Abmeldung fehlgeschlagen.");
|
|
}
|
|
};
|
|
|
|
const handleUseLocation = async () => {
|
|
try {
|
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
if (status !== "granted") {
|
|
Alert.alert("Fehler", "Standortberechtigung nicht erteilt.");
|
|
return;
|
|
}
|
|
const loc = await Location.getCurrentPositionAsync({});
|
|
setLatitude(loc.coords.latitude.toString());
|
|
setLongitude(loc.coords.longitude.toString());
|
|
const addr = await Location.reverseGeocodeAsync({
|
|
latitude: loc.coords.latitude,
|
|
longitude: loc.coords.longitude,
|
|
});
|
|
if (addr && addr.length > 0) {
|
|
setResidence(addr[0].city || addr[0].region || "");
|
|
}
|
|
} catch (e) {
|
|
Alert.alert("Fehler", "Standort konnte nicht ermittelt werden.");
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color="#007AFF" />
|
|
<Text style={styles.loadingText}>Lade App...</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (isLoggedIn && currentUser) {
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.welcomeContainer}>
|
|
<Text style={styles.welcomeTitle}>Willkommen zurück!</Text>
|
|
<Text style={styles.userName}>
|
|
{currentUser.firstName} {currentUser.lastName}
|
|
</Text>
|
|
|
|
<View style={styles.userInfo}>
|
|
<Text style={styles.infoLabel}>E-Mail:</Text>
|
|
<Text style={styles.infoValue}>{currentUser.email}</Text>
|
|
|
|
<Text style={styles.infoLabel}>Mitglied seit:</Text>
|
|
<Text style={styles.infoValue}>
|
|
{new Date(currentUser.createdAt).toLocaleDateString("de-DE")}
|
|
</Text>
|
|
</View>
|
|
|
|
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
|
<Text style={styles.logoutButtonText}>Abmelden</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.submitButton, { marginTop: 20 }]}
|
|
onPress={() => router.push("/calendar")}
|
|
>
|
|
<Text style={styles.submitButtonText}>Zum Kalender</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<KeyboardAvoidingView
|
|
style={styles.keyboardView}
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
>
|
|
<View style={styles.authContainer}>
|
|
<Text style={styles.title}>
|
|
{authMode === "login" ? "Anmelden" : "Registrieren"}
|
|
</Text>
|
|
|
|
{authMode === "register" && (
|
|
<>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Vorname"
|
|
value={firstName}
|
|
onChangeText={setFirstName}
|
|
autoCapitalize="words"
|
|
/>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Nachname"
|
|
value={lastName}
|
|
onChangeText={setLastName}
|
|
autoCapitalize="words"
|
|
/>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Wohnort"
|
|
value={residence}
|
|
onChangeText={setResidence}
|
|
/>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Arbeitsplatz / Uni / Schule"
|
|
value={workplace}
|
|
onChangeText={setWorkplace}
|
|
/>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.submitButton,
|
|
{ backgroundColor: "#34C759", marginBottom: 10 },
|
|
]}
|
|
onPress={handleUseLocation}
|
|
>
|
|
<Text style={styles.submitButtonText}>
|
|
Aktuellen Standort verwenden
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</>
|
|
)}
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="E-Mail"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
/>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Passwort"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
secureTextEntry
|
|
autoCapitalize="none"
|
|
/>
|
|
|
|
<TouchableOpacity
|
|
style={styles.submitButton}
|
|
onPress={authMode === "login" ? handleLogin : handleRegister}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<ActivityIndicator color="white" />
|
|
) : (
|
|
<Text style={styles.submitButtonText}>
|
|
{authMode === "login" ? "Anmelden" : "Registrieren"}
|
|
</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
{biometricSupported && authMode === "login" && (
|
|
<TouchableOpacity
|
|
style={styles.biometricButton}
|
|
onPress={handleBiometricAuth}
|
|
>
|
|
<Text style={styles.biometricButtonText}>
|
|
Mit Face ID / Touch ID anmelden
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
style={styles.switchButton}
|
|
onPress={() =>
|
|
setAuthMode(authMode === "login" ? "register" : "login")
|
|
}
|
|
>
|
|
<Text style={styles.switchButtonText}>
|
|
{authMode === "login"
|
|
? "Noch kein Konto? Registrieren"
|
|
: "Bereits ein Konto? Anmelden"}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: "#f8f9fa",
|
|
},
|
|
keyboardView: {
|
|
flex: 1,
|
|
},
|
|
authContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
paddingHorizontal: 30,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
loadingText: {
|
|
marginTop: 10,
|
|
fontSize: 16,
|
|
color: "#666",
|
|
},
|
|
welcomeContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
paddingHorizontal: 30,
|
|
},
|
|
welcomeTitle: {
|
|
fontSize: 28,
|
|
fontWeight: "bold",
|
|
color: "#333",
|
|
textAlign: "center",
|
|
marginBottom: 10,
|
|
},
|
|
userName: {
|
|
fontSize: 20,
|
|
color: "#666",
|
|
textAlign: "center",
|
|
marginBottom: 40,
|
|
},
|
|
userInfo: {
|
|
backgroundColor: "white",
|
|
padding: 20,
|
|
borderRadius: 10,
|
|
marginBottom: 30,
|
|
elevation: 2,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
},
|
|
infoLabel: {
|
|
fontSize: 14,
|
|
color: "#666",
|
|
marginTop: 10,
|
|
},
|
|
infoValue: {
|
|
fontSize: 16,
|
|
color: "#333",
|
|
fontWeight: "500",
|
|
marginBottom: 5,
|
|
},
|
|
title: {
|
|
fontSize: 32,
|
|
fontWeight: "bold",
|
|
color: "#333",
|
|
textAlign: "center",
|
|
marginBottom: 40,
|
|
},
|
|
input: {
|
|
backgroundColor: "white",
|
|
paddingHorizontal: 15,
|
|
paddingVertical: 15,
|
|
borderRadius: 10,
|
|
fontSize: 16,
|
|
marginBottom: 15,
|
|
elevation: 2,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
},
|
|
submitButton: {
|
|
backgroundColor: "#007AFF",
|
|
paddingVertical: 15,
|
|
borderRadius: 10,
|
|
alignItems: "center",
|
|
marginTop: 10,
|
|
},
|
|
submitButtonText: {
|
|
color: "white",
|
|
fontSize: 18,
|
|
fontWeight: "600",
|
|
},
|
|
biometricButton: {
|
|
backgroundColor: "#34C759",
|
|
paddingVertical: 15,
|
|
borderRadius: 10,
|
|
marginTop: 15,
|
|
alignItems: "center",
|
|
},
|
|
biometricButtonText: {
|
|
color: "white",
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
},
|
|
switchButton: {
|
|
marginTop: 20,
|
|
alignItems: "center",
|
|
},
|
|
switchButtonText: {
|
|
color: "#007AFF",
|
|
fontSize: 16,
|
|
},
|
|
logoutButton: {
|
|
backgroundColor: "#FF3B30",
|
|
paddingVertical: 15,
|
|
borderRadius: 10,
|
|
alignItems: "center",
|
|
},
|
|
logoutButtonText: {
|
|
color: "white",
|
|
fontSize: 18,
|
|
fontWeight: "600",
|
|
},
|
|
});
|