fixed push notifications and auth modified
This commit is contained in:
@@ -87,3 +87,7 @@ frontend/android/
|
|||||||
|
|
||||||
# Expo
|
# Expo
|
||||||
frontend/expo-env.d.ts
|
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
|
## Notes
|
||||||
|
|
||||||
This project is just me messing around with stuff while making an app i will use and perhaps others will find it useful
|
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.JWTService;
|
||||||
import dev.ksan.etfoglasiserver.service.MyUserDetailsService;
|
import dev.ksan.etfoglasiserver.service.MyUserDetailsService;
|
||||||
import dev.ksan.etfoglasiserver.service.UserService;
|
import dev.ksan.etfoglasiserver.service.UserService;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -36,7 +37,12 @@ public class JwtFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
token = authHeader.substring(7);
|
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) {
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package dev.ksan.etfoglasiserver.service;
|
package dev.ksan.etfoglasiserver.service;
|
||||||
|
|
||||||
import dev.ksan.etfoglasiserver.model.User;
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import javax.crypto.KeyGenerator;
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import io.jsonwebtoken.io.Decoders;
|
import io.jsonwebtoken.io.Decoders;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -29,7 +25,7 @@ public class JWTService {
|
|||||||
|
|
||||||
|
|
||||||
return Jwts.builder().claims().add(claims).subject(email)
|
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();
|
.and().signWith(getKey()).compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,20 @@ import {
|
|||||||
onMessage,
|
onMessage,
|
||||||
onNotificationOpenedApp,
|
onNotificationOpenedApp,
|
||||||
getInitialNotification,
|
getInitialNotification,
|
||||||
|
setBackgroundMessageHandler,
|
||||||
} from '@react-native-firebase/messaging';
|
} 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 { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||||
import Toast from 'react-native-toast-message';
|
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() {
|
function NotificationSetup() {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
@@ -27,7 +36,8 @@ function NotificationSetup() {
|
|||||||
console.log('Foreground notification:', msg);
|
console.log('Foreground notification:', msg);
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: 'info',
|
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',
|
position: 'top',
|
||||||
visibilityTime: 4000,
|
visibilityTime: 4000,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import {
|
import {
|
||||||
authApi,
|
authApi,
|
||||||
|
|
||||||
LoginPayload, RegisterPayload, subscriptionsApi, userApi, UserUpdateDTO,
|
LoginPayload, RegisterPayload, setUnauthorizedHandler, subscriptionsApi, userApi, UserUpdateDTO,
|
||||||
} from "@/services/api";
|
} from "@/services/api";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
@@ -45,6 +46,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setUser(u);
|
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') => {
|
const updateNotificationType = async (type: 'PUSH_NOTIFICATION' | 'NO_NOTIFICATION') => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
await userApi.updateNotificationType(type, user.token);
|
await userApi.updateNotificationType(type, user.token);
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ function authHeader(token?: string) {
|
|||||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
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>(
|
async function request<T>(
|
||||||
method: string,
|
method: string,
|
||||||
path: string,
|
path: string,
|
||||||
@@ -30,6 +38,7 @@ async function request<T>(
|
|||||||
console.log('Failed URL:', res.url);
|
console.log('Failed URL:', res.url);
|
||||||
console.log('Status:', res.status);
|
console.log('Status:', res.status);
|
||||||
console.log('Response:', errorText);
|
console.log('Response:', errorText);
|
||||||
|
if (res.status === 401 && token) onUnauthorized?.();
|
||||||
throw new Error(errorText);
|
throw new Error(errorText);
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
onTokenRefresh,
|
onTokenRefresh,
|
||||||
AuthorizationStatus,
|
AuthorizationStatus,
|
||||||
} from '@react-native-firebase/messaging';
|
} from '@react-native-firebase/messaging';
|
||||||
|
import notifee, { AndroidImportance } from '@notifee/react-native';
|
||||||
import { post } from '@/services/api';
|
import { post } from '@/services/api';
|
||||||
|
|
||||||
export async function registerDeviceToken(authToken: string): Promise<void> {
|
export async function registerDeviceToken(authToken: string): Promise<void> {
|
||||||
@@ -32,4 +33,23 @@ export function listenForTokenRefresh(authToken: string): () => void {
|
|||||||
return onTokenRefresh(getMessaging(), async (newToken) => {
|
return onTokenRefresh(getMessaging(), async (newToken) => {
|
||||||
await post('/device-tokens/register', { token: newToken }, authToken);
|
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