Initial commit

Generated by create-expo-app 3.3.0.
This commit is contained in:
Dennis Hundertmark
2025-04-03 08:58:19 +02:00
commit 6ed3300183
113 changed files with 22036 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { Platform } from "react-native";
import { Storage } from "./storage";
import { keycloakConfig } from "./keycloakConfig";
import { ActivityIndicator } from "react-native";
// Only call maybeCompleteAuthSession on web platform
if (Platform.OS === "web") {
WebBrowser.maybeCompleteAuthSession();
}
const TOKEN_KEY = "kc_access_token";
const REFRESH_KEY = "kc_refresh_token";
const ID_TOKEN_KEY = "kc_id_token";
const AuthContext = createContext<any>(null);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [discovery, setDiscovery] =
useState<AuthSession.DiscoveryDocument | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [idToken, setIdToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [isLoaded, setIsLoaded] = useState(false);
const redirectUri = AuthSession.makeRedirectUri({
scheme: keycloakConfig.scheme,
path: Platform.OS === "web" ? "" : "(auth)/index",
});
useEffect(() => {
const loadDiscovery = async () => {
const doc = await AuthSession.fetchDiscoveryAsync(
`${keycloakConfig.url}/realms/${keycloakConfig.realm}`
);
setDiscovery(doc);
};
loadDiscovery();
}, []);
useEffect(() => {
const restore = async () => {
const storedAccess = await Storage.getItem(TOKEN_KEY);
const storedId = await Storage.getItem(ID_TOKEN_KEY);
const storedRefresh = await Storage.getItem(REFRESH_KEY);
if (storedAccess && storedRefresh) {
setAccessToken(storedAccess);
setIdToken(storedId);
setRefreshToken(storedRefresh);
}
setLoading(false);
setIsLoaded(true);
};
restore();
}, []);
const login = async () => {
if (!discovery) return;
const authRequest = new AuthSession.AuthRequest({
responseType: AuthSession.ResponseType.Code,
clientId: keycloakConfig.clientId,
redirectUri,
scopes: ["openid", "profile", "email", "offline_access"],
usePKCE: true,
prompt: AuthSession.Prompt.Login,
});
await authRequest.makeAuthUrlAsync(discovery);
const result = await authRequest.promptAsync(discovery);
if (result.type === "success") {
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: keycloakConfig.clientId,
code: result.params.code,
redirectUri,
extraParams: {
code_verifier: authRequest.codeVerifier || "",
},
},
discovery
);
const { accessToken, idToken, refreshToken } = tokenResponse;
setAccessToken(accessToken);
setIdToken(idToken ?? null);
setRefreshToken(refreshToken ?? null);
await Storage.setItem(TOKEN_KEY, accessToken || "");
if (idToken) await Storage.setItem(ID_TOKEN_KEY, idToken);
if (refreshToken) await Storage.setItem(REFRESH_KEY, refreshToken);
}
};
const refresh = async () => {
if (!discovery || !refreshToken) return;
const newToken = await AuthSession.refreshAsync(
{
clientId: keycloakConfig.clientId,
refreshToken,
},
discovery
);
setAccessToken(newToken.accessToken || null);
setIdToken(newToken.idToken || null);
setRefreshToken(newToken.refreshToken || null);
await Storage.setItem(TOKEN_KEY, newToken.accessToken || "");
if (newToken.idToken) await Storage.setItem(ID_TOKEN_KEY, newToken.idToken);
if (newToken.refreshToken)
await Storage.setItem(REFRESH_KEY, newToken.refreshToken);
};
const logout = async () => {
if (!discovery || !accessToken) return;
const clientId = keycloakConfig.clientId;
const logOutRedirectUri = AuthSession.makeRedirectUri({
scheme: keycloakConfig.scheme,
path: Platform.OS === "web" ? "" : "home",
});
const logoutUrl = `${
discovery.endSessionEndpoint
}?client_id=${clientId}&post_logout_redirect_uri=${encodeURIComponent(
logOutRedirectUri
)}&id_token_hint=${idToken}`;
const result = await WebBrowser.openAuthSessionAsync(
logoutUrl,
logOutRedirectUri
);
if (result.type === "success") {
setAccessToken(null);
setIdToken(null);
setRefreshToken(null);
await Storage.deleteItem(TOKEN_KEY);
await Storage.deleteItem(ID_TOKEN_KEY);
await Storage.deleteItem(REFRESH_KEY);
}
};
if (!discovery || loading) return <ActivityIndicator />;
return (
<AuthContext.Provider
value={{
accessToken,
idToken,
isLoaded,
refreshToken,
login,
logout,
refresh,
isAuthenticated: !!accessToken,
loading,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
+6
View File
@@ -0,0 +1,6 @@
export const keycloakConfig = {
clientId: process.env.EXPO_PUBLIC_KEYCLOAK_CLIENT_ID || "",
realm: process.env.EXPO_PUBLIC_KEYCLOAK_REALM || "",
url: process.env.EXPO_PUBLIC_KEYCLOAK_URL || "",
scheme: process.env.EXPO_PUBLIC_KEYCLOAK_SCHEME || "",
};
+68
View File
@@ -0,0 +1,68 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
/**
* Cross-platform storage utility that works on both web and native platforms.
* Uses SecureStore on native platforms and AsyncStorage on web.
*/
export const Storage = {
/**
* Store a value securely
*/
setItem: async (key: string, value: string): Promise<void> => {
try {
if (Platform.OS === 'web') {
await AsyncStorage.setItem(key, value);
} else {
await SecureStore.setItemAsync(key, value);
}
} catch (error) {
console.error('Error storing value:', error);
}
},
/**
* Retrieve a stored value
*/
getItem: async (key: string): Promise<string | null> => {
try {
if (Platform.OS === 'web') {
return await AsyncStorage.getItem(key);
} else {
return await SecureStore.getItemAsync(key);
}
} catch (error) {
console.error('Error retrieving value:', error);
return null;
}
},
/**
* Delete a stored value
*/
deleteItem: async (key: string): Promise<void> => {
try {
if (Platform.OS === 'web') {
await AsyncStorage.removeItem(key);
} else {
await SecureStore.deleteItemAsync(key);
}
} catch (error) {
console.error('Error deleting value:', error);
}
},
/**
* Check if a key exists in storage
*/
hasItem: async (key: string): Promise<boolean> => {
try {
const value = await Storage.getItem(key);
return value !== null;
} catch (error) {
console.error('Error checking for key:', error);
return false;
}
}
};