Compare commits
2 Commits
14725e180a
...
7d7e641f5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d7e641f5b | |||
| 3afe5e2931 |
@@ -87,3 +87,7 @@ frontend/android/
|
||||
|
||||
# Expo
|
||||
frontend/expo-env.d.ts
|
||||
|
||||
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
@@ -86,3 +86,6 @@ This project is currently under semi-active development as a learning and experi
|
||||
## Notes
|
||||
|
||||
This project is just me messing around with stuff while making an app i will use and perhaps others will find it useful
|
||||
|
||||
|
||||
need to made .creds .env and init directory with subjects.txt
|
||||
|
||||
@@ -3,6 +3,7 @@ package dev.ksan.etfoglasiserver.config;
|
||||
import dev.ksan.etfoglasiserver.service.JWTService;
|
||||
import dev.ksan.etfoglasiserver.service.MyUserDetailsService;
|
||||
import dev.ksan.etfoglasiserver.service.UserService;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -36,7 +37,12 @@ public class JwtFilter extends OncePerRequestFilter {
|
||||
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
token = authHeader.substring(7);
|
||||
username = jwtService.extractEmail(token);
|
||||
try {
|
||||
username = jwtService.extractEmail(token);
|
||||
} catch (JwtException e) {
|
||||
// expired, malformed, or otherwise invalid token - treat as unauthenticated
|
||||
username = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
package dev.ksan.etfoglasiserver.service;
|
||||
|
||||
import dev.ksan.etfoglasiserver.model.User;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -29,7 +25,7 @@ public class JWTService {
|
||||
|
||||
|
||||
return Jwts.builder().claims().add(claims).subject(email)
|
||||
.issuedAt(new Date(System.currentTimeMillis())).expiration(new Date(System.currentTimeMillis()+60*60*300))
|
||||
.issuedAt(new Date(System.currentTimeMillis())).expiration(new Date(System.currentTimeMillis()+1000L*60*60*24*60))
|
||||
.and().signWith(getKey()).compact();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,20 @@ import {
|
||||
onMessage,
|
||||
onNotificationOpenedApp,
|
||||
getInitialNotification,
|
||||
setBackgroundMessageHandler,
|
||||
} from '@react-native-firebase/messaging';
|
||||
import { registerDeviceToken, listenForTokenRefresh } from '@/services/notifications';
|
||||
import { registerDeviceToken, listenForTokenRefresh, displayLocalNotification } from '@/services/notifications';
|
||||
import { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
// Registered at module scope so it's installed as soon as this entry file
|
||||
// loads, which is required for it to fire while the app is backgrounded/killed.
|
||||
setBackgroundMessageHandler(getMessaging(), async (remoteMessage) => {
|
||||
const title = remoteMessage.data?.title as string | undefined;
|
||||
const body = remoteMessage.data?.body as string | undefined;
|
||||
await displayLocalNotification(title, body);
|
||||
});
|
||||
|
||||
function NotificationSetup() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
@@ -27,7 +36,8 @@ function NotificationSetup() {
|
||||
console.log('Foreground notification:', msg);
|
||||
Toast.show({
|
||||
type: 'info',
|
||||
text1: msg.notification?.title ?? 'New Notification',
|
||||
text1: (msg.data?.title as string | undefined) ?? 'New Notification',
|
||||
text2: msg.data?.body as string | undefined,
|
||||
position: 'top',
|
||||
visibilityTime: 4000,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import Toast from "react-native-toast-message";
|
||||
import {
|
||||
authApi,
|
||||
|
||||
LoginPayload, RegisterPayload, subscriptionsApi, userApi, UserUpdateDTO,
|
||||
LoginPayload, RegisterPayload, setUnauthorizedHandler, subscriptionsApi, userApi, UserUpdateDTO,
|
||||
} from "@/services/api";
|
||||
|
||||
type User = {
|
||||
@@ -45,6 +46,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setUser(u);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setUnauthorizedHandler(() => {
|
||||
persist(null);
|
||||
Toast.show({
|
||||
type: 'info',
|
||||
text1: 'Session expired',
|
||||
text2: 'Please sign in again.',
|
||||
position: 'top',
|
||||
visibilityTime: 4000,
|
||||
});
|
||||
});
|
||||
return () => setUnauthorizedHandler(null);
|
||||
}, []);
|
||||
|
||||
const updateNotificationType = async (type: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION') => {
|
||||
if (!user) return;
|
||||
await userApi.updateNotificationType(type, user.token);
|
||||
|
||||
+56
-34
@@ -2,46 +2,69 @@
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
import React, {useState} from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { NavigationContainer, DefaultTheme, DarkTheme } from "@react-navigation/native";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { enableScreens } from "react-native-screens";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import messaging from "@react-native-firebase/messaging";
|
||||
import notifee, { AndroidImportance } from "@notifee/react-native";
|
||||
|
||||
import SubscribedFeed from "@/screens/SubscribedFeed";
|
||||
import AllFeed from "@/screens/AllFeed";
|
||||
import Profile from "@/screens/Profile";
|
||||
import {enableScreens} from "react-native-screens";
|
||||
import {SafeAreaProvider} from "react-native-safe-area-context";
|
||||
import AuthGate from "@/screens/AuthGate";
|
||||
import {AuthProvider, useAuth} from "@/context/AuthContext";
|
||||
import messaging, {setBackgroundMessageHandler} from "@react-native-firebase/messaging";
|
||||
|
||||
import { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||
import { useUpdatecheck } from "./hooks/useUpdatecheck";
|
||||
import { UpdatePrompt } from './components/UpdatePrompt';
|
||||
import { UpdatePrompt } from "./components/UpdatePrompt";
|
||||
import { usePushNotifications } from "./hooks/usePushNotifications";
|
||||
|
||||
messaging().setBackgroundMessageHandler(async () => {});
|
||||
// Handles data-only FCM messages when the app is in the background or closed.
|
||||
messaging().setBackgroundMessageHandler(async remoteMessage => {
|
||||
const title = remoteMessage.data?.title as string | undefined;
|
||||
const body = remoteMessage.data?.body as string | undefined;
|
||||
|
||||
if (!title && !body) return;
|
||||
|
||||
const channelId = await notifee.createChannel({
|
||||
id: "default",
|
||||
name: "General",
|
||||
importance: AndroidImportance.HIGH,
|
||||
});
|
||||
|
||||
await notifee.displayNotification({
|
||||
title,
|
||||
body,
|
||||
android: {
|
||||
channelId,
|
||||
pressAction: { id: "default" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Must be called before any navigator renders
|
||||
enableScreens();
|
||||
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const LIGHT_TAB = {
|
||||
bg: "#FFFFFF",
|
||||
border: "#E8E2D5",
|
||||
active: "#C4622D",
|
||||
inactive: "#8A8278",
|
||||
label: "#1A1714",
|
||||
};
|
||||
const DARK_TAB = {
|
||||
bg: "#161513",
|
||||
border: "#2C2A27",
|
||||
active: "#E07B45",
|
||||
inactive: "#706D67",
|
||||
label: "#F0EDE8",
|
||||
bg: "#FFFFFF",
|
||||
border: "#E8E2D5",
|
||||
active: "#C4622D",
|
||||
inactive: "#8A8278",
|
||||
label: "#1A1714",
|
||||
};
|
||||
|
||||
const DARK_TAB = {
|
||||
bg: "#161513",
|
||||
border: "#2C2A27",
|
||||
active: "#E07B45",
|
||||
inactive: "#706D67",
|
||||
label: "#F0EDE8",
|
||||
};
|
||||
|
||||
function ProfileTab() {
|
||||
const { user, loading } = useAuth();
|
||||
@@ -56,13 +79,12 @@ export default function App() {
|
||||
const tab = dark ? DARK_TAB : LIGHT_TAB;
|
||||
|
||||
const navTheme = dark
|
||||
? { ...DarkTheme, colors: { ...DarkTheme.colors, background: "#0E0D0C" } }
|
||||
? { ...DarkTheme, colors: { ...DarkTheme.colors, background: "#0E0D0C" } }
|
||||
: { ...DefaultTheme, colors: { ...DefaultTheme.colors, background: "#FDFAF5" } };
|
||||
|
||||
const { updateInfo } = useUpdatecheck();
|
||||
const [updateDismissed, setUpdateDismissed] = useState(false);
|
||||
|
||||
|
||||
usePushNotifications();
|
||||
|
||||
return (
|
||||
@@ -75,19 +97,19 @@ export default function App() {
|
||||
|
||||
tabBarStyle: {
|
||||
backgroundColor: tab.bg,
|
||||
borderTopColor: tab.border,
|
||||
borderTopWidth: 1,
|
||||
height: 60,
|
||||
paddingBottom: 8,
|
||||
paddingTop: 6,
|
||||
borderTopColor: tab.border,
|
||||
borderTopWidth: 1,
|
||||
height: 60,
|
||||
paddingBottom: 8,
|
||||
paddingTop: 6,
|
||||
},
|
||||
|
||||
tabBarActiveTintColor: tab.active,
|
||||
tabBarInactiveTintColor: tab.inactive,
|
||||
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 10,
|
||||
fontWeight: "600",
|
||||
fontSize: 10,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
|
||||
@@ -96,9 +118,9 @@ export default function App() {
|
||||
string,
|
||||
{ active: keyof typeof Ionicons.glyphMap; inactive: keyof typeof Ionicons.glyphMap }
|
||||
> = {
|
||||
"Subscribed": { active: "bookmark", inactive: "bookmark-outline" },
|
||||
"Discover": { active: "compass", inactive: "compass-outline" },
|
||||
"Profile": { active: "person-circle", inactive: "person-circle-outline" },
|
||||
"Subscribed": { active: "bookmark", inactive: "bookmark-outline" },
|
||||
"Discover": { active: "compass", inactive: "compass-outline" },
|
||||
"Profile": { active: "person-circle", inactive: "person-circle-outline" },
|
||||
};
|
||||
const set = icons[route.name];
|
||||
|
||||
@@ -130,7 +152,7 @@ export default function App() {
|
||||
</Tab.Navigator>
|
||||
</NavigationContainer>
|
||||
|
||||
{/* Overlays the entire app, including the nav bar */}
|
||||
{/* Overlays the entire app including the tab bar */}
|
||||
{updateInfo && !updateDismissed && (
|
||||
<UpdatePrompt
|
||||
updateInfo={updateInfo}
|
||||
@@ -141,4 +163,4 @@ export default function App() {
|
||||
</SafeAreaProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,14 @@ function authHeader(token?: string) {
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
let onUnauthorized: (() => void) | null = null;
|
||||
|
||||
// Called once from AuthProvider so api.tsx can trigger a logout/relogin
|
||||
// when the backend rejects a request due to a missing/expired/invalid token.
|
||||
export function setUnauthorizedHandler(handler: (() => void) | null) {
|
||||
onUnauthorized = handler;
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
@@ -30,6 +38,7 @@ async function request<T>(
|
||||
console.log('Failed URL:', res.url);
|
||||
console.log('Status:', res.status);
|
||||
console.log('Response:', errorText);
|
||||
if (res.status === 401 && token) onUnauthorized?.();
|
||||
throw new Error(errorText);
|
||||
}
|
||||
return res.json();
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
onTokenRefresh,
|
||||
AuthorizationStatus,
|
||||
} from '@react-native-firebase/messaging';
|
||||
import notifee, { AndroidImportance } from '@notifee/react-native';
|
||||
import { post } from '@/services/api';
|
||||
|
||||
export async function registerDeviceToken(authToken: string): Promise<void> {
|
||||
@@ -32,4 +33,23 @@ export function listenForTokenRefresh(authToken: string): () => void {
|
||||
return onTokenRefresh(getMessaging(), async (newToken) => {
|
||||
await post('/device-tokens/register', { token: newToken }, authToken);
|
||||
});
|
||||
}
|
||||
|
||||
export async function displayLocalNotification(title?: string, body?: string) {
|
||||
if (!title && !body) return;
|
||||
|
||||
const channelId = await notifee.createChannel({
|
||||
id: 'default',
|
||||
name: 'General',
|
||||
importance: AndroidImportance.HIGH,
|
||||
});
|
||||
|
||||
await notifee.displayNotification({
|
||||
title,
|
||||
body,
|
||||
android: {
|
||||
channelId,
|
||||
pressAction: { id: 'default' },
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user