chore: Refactor RestService usage into a singleton pattern and remove redundant JWT token dependency

This commit is contained in:
2025-12-27 05:06:37 +01:00
parent c1034b1c21
commit 63e2463c20
8 changed files with 57 additions and 59 deletions
+5 -7
View File
@@ -12,16 +12,14 @@ import {
Linking,
Modal,
} from "react-native";
import {S3File, RestService} from "@/src/services/RestService";
import {S3File, restService} from "@/src/services/RestService";
import ThemedButton from "@/src/components/themed/ThemedButton";
import {useColors} from "@/src/hooks/useColors";
import {useAuth} from "@/src/stores/authStore";
import {MaterialIcons} from '@expo/vector-icons';
import { useMatrixStore } from "@/src/stores";
import SaveToMatrixButton from "@/src/components/SaveToMatrixButton";
export default function ImageScreen() {
const {token} = useAuth();
const [uploading, setUploading] = useState(false);
const [files, setFiles] = useState<S3File[]>([]);
const [showFiles, setShowFiles] = useState(false);
@@ -35,7 +33,7 @@ export default function ImageScreen() {
const fetchStoredFiles = async () => {
setLoadingFiles(true);
try {
const response = await new RestService(token).getStoredFiles();
const response = await restService.getStoredFiles();
if (response.ok && response.data) {
setFiles(response.data);
setShowFiles(true);
@@ -68,7 +66,7 @@ export default function ImageScreen() {
formData.append('image', fileBlob, fileName);
console.log("Web-Plattform: Blob angehängt mit Namen:", fileName);
const response = await new RestService(token).uploadFile(formData);
const response = await restService.uploadFile(formData);
if (response.ok) {
console.log("Datei erfolgreich hochgeladen");
@@ -117,7 +115,7 @@ export default function ImageScreen() {
const viewFile = async (objectKey: string) => {
try {
const response = await new RestService(token).getFileUrl(objectKey);
const response = await restService.getFileUrl(objectKey);
if (response.ok && response.data.url) {
const url = response.data.url;
@@ -137,7 +135,7 @@ export default function ImageScreen() {
const deleteFile = async (objectKey: string) => {
try {
const response = await new RestService(token).deleteFile(objectKey);
const response = await restService.deleteFile(objectKey);
if (response.ok) {
console.log("Datei erfolgreich gelöscht");
fetchStoredFiles();
+4 -10
View File
@@ -4,7 +4,7 @@ import ThemedBackground from "@/src/components/themed/ThemedBackground";
import ChangePasswordFeature from "@/src/components/ChangePasswordFeature";
import ThemeToggleButton from "@/src/components/ThemeToggleButton";
import SpotifyAuthButton from "@/src/components/SpotifyAuthButton";
import {RestService, Token} from "@/src/services/RestService";
import {restService, Token} from "@/src/services/RestService";
import {useAuth} from "@/src/stores/authStore";
import {View, Text} from "react-native";
@@ -12,7 +12,7 @@ import ThemedButton from "@/src/components/themed/ThemedButton";
import {useRouter} from "expo-router";
export default function SettingsScreen() {
const {token: jwtToken, authenticatedUser, logout, refreshUser} = useAuth();
const {authenticatedUser, logout, refreshUser} = useAuth();
const router = useRouter();
const handleAuthSuccess = (token: Token) => {
@@ -23,7 +23,7 @@ export default function SettingsScreen() {
expirationDate: new Date(Date.now() + token.expires_in * 1000),
};
new RestService(jwtToken).updateSelfSpotifyConfig(spotifyConfig).then((result) => {
restService.updateSelfSpotifyConfig(spotifyConfig).then((result) => {
console.log("Spotify Token gespeichert");
console.log(result);
@@ -38,7 +38,6 @@ export default function SettingsScreen() {
Hallo, {authenticatedUser?.name}
</ThemedHeader>
{/* Erscheinungsbild Section */}
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Erscheinungsbild
@@ -51,7 +50,6 @@ export default function SettingsScreen() {
</View>
</View>
{/* Konto Section */}
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Konto
@@ -61,7 +59,6 @@ export default function SettingsScreen() {
</View>
</View>
{/* Integrationen Section */}
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Integrationen
@@ -69,7 +66,6 @@ export default function SettingsScreen() {
<View className="gap-3">
<SpotifyAuthButton
onAuthSuccess={handleAuthSuccess}
jwtToken={jwtToken}
disabled={!!authenticatedUser?.spotifyConfig}
/>
{!!authenticatedUser?.spotifyConfig && (
@@ -77,8 +73,7 @@ export default function SettingsScreen() {
mode="outlined"
title="Spotify trennen"
onPress={() => {
const rest = new RestService(jwtToken);
rest.removeSpotifyConfig().then((result) => {
restService.removeSpotifyConfig().then((result) => {
console.log("Spotify Login entfernt");
console.log(result);
refreshUser();
@@ -89,7 +84,6 @@ export default function SettingsScreen() {
</View>
</View>
{/* Logout am Ende */}
<View className="mt-auto pb-4">
<ThemedButton
mode="outlined"
+2 -4
View File
@@ -1,7 +1,6 @@
import React, {useRef, useState} from "react";
import { TextInput, View } from "react-native";
import { ApiResponse, RestService } from "@/src/services/RestService";
import { useAuth } from "@/src/stores/authStore";
import { ApiResponse, restService } from "@/src/services/RestService";
import PasswordInput from "@/src/components/PasswordInput";
import ThemedButton from "@/src/components/themed/ThemedButton";
import { Text } from "react-native-paper";
@@ -13,7 +12,6 @@ interface ChangePasswordFormProps {
}
export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswordFormProps) {
const { token: jwtToken } = useAuth();
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [apiResponse, setApiResponse] = useState<ApiResponse<{ message: string }> | null>(null);
@@ -30,7 +28,7 @@ export default function ChangePasswordForm({ onSuccess, onCancel }: ChangePasswo
return;
}
const response = await new RestService(jwtToken).changeSelfPassword(password, confirmPassword);
const response = await restService.changeSelfPassword(password, confirmPassword);
setApiResponse(response);
if (response.ok) {
+2 -4
View File
@@ -2,8 +2,7 @@ import React, { useState } from "react";
import { View, Text } from "react-native";
import ThemedButton from "@/src/components/themed/ThemedButton";
import { useMatrixStore } from "@/src/stores";
import { useAuth } from "@/src/stores/authStore";
import { RestService } from "@/src/services/RestService";
import { restService } from "@/src/services/RestService";
import { MatrixState } from "@/src/model/User";
interface SaveToMatrixButtonProps {
@@ -12,7 +11,6 @@ interface SaveToMatrixButtonProps {
}
export default function SaveToMatrixButton({ mode, className }: SaveToMatrixButtonProps) {
const { token } = useAuth();
const setGlobalMode = useMatrixStore((s) => s.setGlobalMode);
const [saving, setSaving] = useState(false);
@@ -29,7 +27,7 @@ export default function SaveToMatrixButton({ mode, className }: SaveToMatrixButt
const updatedState = useMatrixStore.getState().matrixState;
const response = await new RestService(token).updateLastState(updatedState);
const response = await restService.updateLastState(updatedState);
if (response.ok) {
setFeedback({ type: 'success', message: 'Gespeichert!' });
+2 -3
View File
@@ -5,12 +5,11 @@ import ThemedButton from "@/src/components/themed/ThemedButton";
interface SpotifyAuthButtonProps {
onAuthSuccess: (token: Token) => void;
jwtToken: string | null;
disabled: boolean;
}
const SpotifyAuthButton = ({onAuthSuccess, jwtToken, disabled}: SpotifyAuthButtonProps) => {
const {promptAuth, isReady, error} = useSpotifyAuth(onAuthSuccess, jwtToken);
const SpotifyAuthButton = ({onAuthSuccess, disabled}: SpotifyAuthButtonProps) => {
const {promptAuth, isReady, error} = useSpotifyAuth(onAuthSuccess);
if (error) {
console.error('Spotify Auth Error:', error);
+2 -3
View File
@@ -1,6 +1,6 @@
import {useEffect} from "react";
import {makeRedirectUri, useAuthRequest} from "expo-auth-session";
import {RestService, Token} from "@/src/services/RestService";
import {restService, Token} from "@/src/services/RestService";
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
@@ -15,7 +15,6 @@ interface UseSpotifyAuthResult {
export const useSpotifyAuth = (
onAuthSuccess: (token: Token) => void,
jwtToken: string | null,
): UseSpotifyAuthResult => {
const [request, response, promptAsync] = useAuthRequest(
{
@@ -35,7 +34,7 @@ export const useSpotifyAuth = (
if (response?.type === 'success') {
try {
const {code} = response.params;
const res = (await new RestService(jwtToken).exchangeSpotifyCodeForToken(code));
const res = (await restService.exchangeSpotifyCodeForToken(code));
if (!res.ok) {
console.log("Fehler beim token abrufen");
return;
+24 -12
View File
@@ -26,13 +26,18 @@ export interface S3File {
size: number;
}
// Token provider function type - will be set from authStore
type TokenProvider = () => string | null;
let tokenProvider: TokenProvider = () => null;
export const setTokenProvider = (provider: TokenProvider) => {
tokenProvider = provider;
};
class RestService {
private readonly jwtToken: string | null;
private api: AxiosInstance;
constructor(jwtToken: string | null) {
this.jwtToken = jwtToken;
constructor() {
this.api = axios.create({
baseURL: API_URL,
timeout: 10000, // Set a timeout for requests
@@ -41,25 +46,29 @@ class RestService {
this.api.interceptors.request.use(
(config) => {
if (this.jwtToken) {
config.headers.Authorization = `Bearer ${this.jwtToken}`;
// Only use token for non-web platforms
if (Platform.OS !== 'web') {
const token = tokenProvider();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
console.log('Request Config:', config);
return config;
},
(error) => {
console.error('Request Error:', error);
return Promise.reject(error);
}
);
this.api.interceptors.response.use(
(response) => {
console.log('Response Data:', response.data);
return response;
},
(error) => {
console.error('Response Error:', error.response?.data || error.message);
// Only log non-connection errors to avoid spam when backend is down
if (error.code !== 'ECONNREFUSED' && error.code !== 'ERR_NETWORK') {
console.error('Response Error:', error.response?.data || error.message);
}
return Promise.reject(error);
}
);
@@ -207,10 +216,13 @@ class RestService {
);
return response.data;
} catch (error) {
console.error('Error during request:', error);
// Silently throw connection errors to avoid log spam
throw error;
}
}
}
export {RestService};
// Singleton instance
const restService = new RestService();
export { RestService, restService };
+16 -16
View File
@@ -3,7 +3,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
import { Platform } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { User } from '@/src/model/User';
import { RestService } from '@/src/services/RestService';
import { restService, setTokenProvider } from '@/src/services/RestService';
import { useMatrixStore } from './matrixStore';
const authStorage = {
@@ -69,14 +69,12 @@ export const useAuthStore = create<AuthState>()(
checkAuthStatus: async () => {
const state = get();
try {
const token = Platform.OS === 'web' ? null : state.token;
if (Platform.OS !== 'web' && !token) {
if (Platform.OS !== 'web' && !state.token) {
set({ isAuthenticated: false, loading: false });
return;
}
const user = await fetchUserAndInitMatrix(token);
const user = await fetchUserAndInitMatrix();
set({
isAuthenticated: !!user,
authenticatedUser: user,
@@ -102,7 +100,7 @@ export const useAuthStore = create<AuthState>()(
set({ loading: true, error: null });
try {
const response = await new RestService(null).login(username, password, stayLoggedIn);
const response = await restService.login(username, password, stayLoggedIn);
if (!response.ok) {
console.error("Login failed:", response.data);
@@ -118,12 +116,13 @@ export const useAuthStore = create<AuthState>()(
let token: string | null = null;
if (Platform.OS !== 'web') {
token = response.data.token!;
// Set token in state first so the token provider has access to it
set({ token });
}
const user = await fetchUserAndInitMatrix(token);
const user = await fetchUserAndInitMatrix();
set({
token,
error: null,
isAuthenticated: !!user,
authenticatedUser: user,
@@ -141,7 +140,7 @@ export const useAuthStore = create<AuthState>()(
logout: async () => {
try {
if (Platform.OS === 'web') {
await new RestService(null).logout();
await restService.logout();
}
} finally {
useMatrixStore.getState().resetToDefaults();
@@ -158,10 +157,9 @@ export const useAuthStore = create<AuthState>()(
const state = get();
console.log("refreshUser");
const token = Platform.OS === 'web' ? null : state.token;
if (Platform.OS !== 'web' && !token) return;
if (Platform.OS !== 'web' && !state.token) return;
const user = await fetchUserAndInitMatrix(token);
const user = await fetchUserAndInitMatrix();
if (user) {
set({ authenticatedUser: user });
}
@@ -173,6 +171,8 @@ export const useAuthStore = create<AuthState>()(
partialize: (state) => ({ token: state.token }),
onRehydrateStorage: () => (state) => {
if (state) {
// Set the token provider so RestService can access the token
setTokenProvider(() => useAuthStore.getState().token);
state.setHydrated();
state.checkAuthStatus();
}
@@ -181,16 +181,16 @@ export const useAuthStore = create<AuthState>()(
)
);
async function fetchUser(token: string | null): Promise<User | null> {
const response = await new RestService(token).getSelf();
async function fetchUser(): Promise<User | null> {
const response = await restService.getSelf();
if (!response.ok || !response.data) {
return null;
}
return response.data;
}
async function fetchUserAndInitMatrix(token: string | null): Promise<User | null> {
const user = await fetchUser(token);
async function fetchUserAndInitMatrix(): Promise<User | null> {
const user = await fetchUser();
if (user?.lastState) {
useMatrixStore.getState().initializeFromUser(user.lastState);
}