CI/CD: Add GitLab CI/CD pipeline for Hostinger deployment
- Updated .gitlab-ci.yml with complete build, test, and deploy stages - Added authentication redirect fix in Program.cs (302 redirect for admin routes) - Fixed Cookie vs Bearer authentication conflict for admin panel - Configure pipeline to build from .NET 9.0 source - Deploy to Hostinger VPS with proper environment variables - Include rollback capability for production deployments 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -140,12 +140,33 @@ if (string.IsNullOrEmpty(jwtKey))
|
||||
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "LittleShop";
|
||||
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "LittleShop";
|
||||
|
||||
builder.Services.AddAuthentication("Cookies")
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = "Cookies";
|
||||
options.DefaultChallengeScheme = "Cookies";
|
||||
})
|
||||
.AddCookie("Cookies", options =>
|
||||
{
|
||||
options.LoginPath = "/Admin/Account/Login";
|
||||
options.LogoutPath = "/Admin/Account/Logout";
|
||||
options.AccessDeniedPath = "/Admin/Account/AccessDenied";
|
||||
options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents
|
||||
{
|
||||
OnRedirectToLogin = context =>
|
||||
{
|
||||
// For admin routes, always redirect to login page
|
||||
if (context.Request.Path.StartsWithSegments("/Admin"))
|
||||
{
|
||||
context.Response.StatusCode = 302;
|
||||
context.Response.Headers["Location"] = context.RedirectUri;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// For API routes, return 401
|
||||
context.Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
})
|
||||
.AddJwtBearer("Bearer", options =>
|
||||
{
|
||||
@@ -166,7 +187,7 @@ builder.Services.AddAuthorization(options =>
|
||||
options.AddPolicy("AdminOnly", policy =>
|
||||
policy.RequireAuthenticatedUser()
|
||||
.RequireRole("Admin")
|
||||
.AddAuthenticationSchemes("Cookies", "Bearer")); // Support both cookie and JWT
|
||||
.AddAuthenticationSchemes("Cookies")); // Only use cookies for admin panel
|
||||
options.AddPolicy("ApiAccess", policy =>
|
||||
policy.RequireAuthenticatedUser()
|
||||
.AddAuthenticationSchemes("Bearer")); // JWT only for API access
|
||||
|
||||
670
LittleShop/wwwroot/js/pwa-fixed.js
Normal file
670
LittleShop/wwwroot/js/pwa-fixed.js
Normal file
@@ -0,0 +1,670 @@
|
||||
// Progressive Web App functionality with fixes for desktop and persistent prompts
|
||||
// Handles service worker registration and PWA features
|
||||
|
||||
class PWAManager {
|
||||
constructor() {
|
||||
this.swRegistration = null;
|
||||
this.vapidPublicKey = null;
|
||||
this.pushSubscription = null;
|
||||
this.installPromptShown = false;
|
||||
this.pushPromptShown = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
console.log('PWA: Initializing PWA Manager...');
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
this.swRegistration = await navigator.serviceWorker.register('/sw.js');
|
||||
console.log('SW: Service Worker registered successfully');
|
||||
|
||||
// Listen for updates
|
||||
this.swRegistration.addEventListener('updatefound', () => {
|
||||
console.log('SW: New version available');
|
||||
this.showUpdateNotification();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('SW: Service Worker registration failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup PWA install prompt
|
||||
this.setupInstallPrompt();
|
||||
|
||||
// Setup notifications (if enabled)
|
||||
this.setupNotifications();
|
||||
|
||||
// Setup push notifications
|
||||
this.setupPushNotifications();
|
||||
}
|
||||
|
||||
setupInstallPrompt() {
|
||||
let deferredPrompt;
|
||||
|
||||
// Check if already installed on init
|
||||
const isInstalled = this.isInstalled();
|
||||
if (isInstalled) {
|
||||
console.log('PWA: App is already installed');
|
||||
localStorage.setItem('pwaInstalled', 'true');
|
||||
this.installPromptShown = true; // Don't show prompt if already installed
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
console.log('PWA: beforeinstallprompt event fired');
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
|
||||
// Only show if not already shown and not installed
|
||||
if (!this.installPromptShown && !this.isInstalled()) {
|
||||
this.showInstallButton(deferredPrompt);
|
||||
this.installPromptShown = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('PWA: App was installed');
|
||||
localStorage.setItem('pwaInstalled', 'true');
|
||||
this.hideInstallButton();
|
||||
this.installPromptShown = true;
|
||||
});
|
||||
|
||||
// Only show manual button if:
|
||||
// 1. Not installed
|
||||
// 2. Not already shown
|
||||
// 3. User hasn't dismissed it
|
||||
const installDismissed = localStorage.getItem('pwaInstallDismissed');
|
||||
if (!isInstalled && !this.installPromptShown && !installDismissed) {
|
||||
// Wait for browser prompt opportunity
|
||||
setTimeout(() => {
|
||||
if (!this.installPromptShown && !this.isInstalled()) {
|
||||
console.log('PWA: Showing manual install option');
|
||||
this.showManualInstallButton();
|
||||
this.installPromptShown = true;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
showInstallButton(deferredPrompt) {
|
||||
// Check again before showing
|
||||
if (this.isInstalled() || document.getElementById('pwa-install-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installBtn = document.createElement('button');
|
||||
installBtn.id = 'pwa-install-btn';
|
||||
installBtn.className = 'btn btn-primary btn-sm';
|
||||
installBtn.innerHTML = '<i class="fas fa-download"></i> Install App';
|
||||
installBtn.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
`;
|
||||
|
||||
// Add close button
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'btn-close btn-close-white';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
`;
|
||||
closeBtn.onclick = () => {
|
||||
localStorage.setItem('pwaInstallDismissed', 'true');
|
||||
this.hideInstallButton();
|
||||
};
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.id = 'pwa-install-wrapper';
|
||||
wrapper.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
`;
|
||||
wrapper.appendChild(installBtn);
|
||||
wrapper.appendChild(closeBtn);
|
||||
|
||||
installBtn.addEventListener('click', async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
console.log('PWA: User response to install prompt:', outcome);
|
||||
if (outcome === 'accepted') {
|
||||
localStorage.setItem('pwaInstalled', 'true');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
this.hideInstallButton();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(wrapper);
|
||||
}
|
||||
|
||||
hideInstallButton() {
|
||||
const wrapper = document.getElementById('pwa-install-wrapper');
|
||||
const btn = document.getElementById('pwa-install-btn');
|
||||
if (wrapper) wrapper.remove();
|
||||
if (btn) btn.remove();
|
||||
}
|
||||
|
||||
showUpdateNotification() {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'alert alert-info alert-dismissible';
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<strong>Update Available!</strong><br>
|
||||
A new version of the app is ready.
|
||||
<button type="button" class="btn btn-sm btn-outline-info ms-2" id="update-btn">
|
||||
Update Now
|
||||
</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
document.getElementById('update-btn').addEventListener('click', () => {
|
||||
if (this.swRegistration && this.swRegistration.waiting) {
|
||||
this.swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setupNotifications() {
|
||||
if ('Notification' in window) {
|
||||
const permission = await this.requestNotificationPermission();
|
||||
console.log('Notifications permission:', permission);
|
||||
}
|
||||
}
|
||||
|
||||
async requestNotificationPermission() {
|
||||
if (Notification.permission === 'default') {
|
||||
return Notification.permission;
|
||||
}
|
||||
return Notification.permission;
|
||||
}
|
||||
|
||||
showNotification(title, options = {}) {
|
||||
if (Notification.permission === 'granted') {
|
||||
const notification = new Notification(title, {
|
||||
icon: '/icons/icon-192x192.png',
|
||||
badge: '/icons/icon-72x72.png',
|
||||
tag: 'littleshop-admin',
|
||||
...options
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
}, 5000);
|
||||
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
showManualInstallButton() {
|
||||
if (this.isInstalled() || document.getElementById('pwa-install-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installBtn = document.createElement('button');
|
||||
installBtn.id = 'pwa-install-btn';
|
||||
installBtn.className = 'btn btn-primary btn-sm';
|
||||
installBtn.innerHTML = '<i class="fas fa-mobile-alt"></i> Install as App';
|
||||
installBtn.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
`;
|
||||
|
||||
// Add close button
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'btn-close btn-close-white';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
`;
|
||||
closeBtn.onclick = () => {
|
||||
localStorage.setItem('pwaInstallDismissed', 'true');
|
||||
this.hideInstallButton();
|
||||
};
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.id = 'pwa-install-wrapper';
|
||||
wrapper.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
`;
|
||||
wrapper.appendChild(installBtn);
|
||||
wrapper.appendChild(closeBtn);
|
||||
|
||||
installBtn.addEventListener('click', () => {
|
||||
const isChrome = navigator.userAgent.includes('Chrome');
|
||||
const isEdge = navigator.userAgent.includes('Edge');
|
||||
const isFirefox = navigator.userAgent.includes('Firefox');
|
||||
|
||||
let instructions = 'To install this app:\n\n';
|
||||
|
||||
if (isChrome || isEdge) {
|
||||
instructions += '1. Look for the install icon (⬇️) in the address bar\n';
|
||||
instructions += '2. Or click the browser menu (⋮) → "Install LittleShop Admin"\n';
|
||||
instructions += '3. Or check if there\'s an "Install app" option in the browser menu';
|
||||
} else if (isFirefox) {
|
||||
instructions += '1. Firefox doesn\'t support PWA installation yet\n';
|
||||
instructions += '2. You can bookmark this page for easy access\n';
|
||||
instructions += '3. Or use Chrome/Edge for the full PWA experience';
|
||||
} else {
|
||||
instructions += '1. Look for an install or "Add to Home Screen" option\n';
|
||||
instructions += '2. Check your browser menu for app installation\n';
|
||||
instructions += '3. Or bookmark this page for quick access';
|
||||
}
|
||||
|
||||
alert(instructions);
|
||||
localStorage.setItem('pwaInstallDismissed', 'true');
|
||||
this.hideInstallButton();
|
||||
});
|
||||
|
||||
document.body.appendChild(wrapper);
|
||||
}
|
||||
|
||||
isInstalled() {
|
||||
// Check multiple indicators
|
||||
const standalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||
const iosStandalone = window.navigator.standalone === true;
|
||||
const localStorageFlag = localStorage.getItem('pwaInstalled') === 'true';
|
||||
|
||||
return standalone || iosStandalone || localStorageFlag;
|
||||
}
|
||||
|
||||
async setupPushNotifications() {
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||
console.log('PWA: Push notifications not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.getVapidPublicKey();
|
||||
await this.checkPushSubscription();
|
||||
|
||||
// Only show prompt if:
|
||||
// 1. Not subscribed
|
||||
// 2. Not already shown
|
||||
// 3. User hasn't declined
|
||||
if (!this.pushSubscription && !this.pushPromptShown) {
|
||||
const userDeclined = localStorage.getItem('pushNotificationDeclined');
|
||||
|
||||
if (!userDeclined) {
|
||||
// Delay showing the prompt to avoid overwhelming user
|
||||
setTimeout(() => {
|
||||
if (!this.pushSubscription && !this.pushPromptShown) {
|
||||
this.showPushNotificationSetup();
|
||||
this.pushPromptShown = true;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('PWA: Failed to setup push notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getVapidPublicKey() {
|
||||
try {
|
||||
const response = await fetch('/api/push/vapid-key');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.vapidPublicKey = data.publicKey;
|
||||
console.log('PWA: VAPID public key retrieved');
|
||||
} else {
|
||||
throw new Error('Failed to get VAPID public key');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PWA: Error getting VAPID public key:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async checkPushSubscription() {
|
||||
if (!this.swRegistration) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.pushSubscription = await this.swRegistration.pushManager.getSubscription();
|
||||
if (this.pushSubscription) {
|
||||
console.log('PWA: User has active push subscription');
|
||||
localStorage.setItem('pushSubscribed', 'true');
|
||||
} else {
|
||||
console.log('PWA: User is not subscribed to push notifications');
|
||||
localStorage.removeItem('pushSubscribed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PWA: Error checking push subscription:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async subscribeToPushNotifications() {
|
||||
if (!this.swRegistration || !this.vapidPublicKey) {
|
||||
throw new Error('Service worker or VAPID key not available');
|
||||
}
|
||||
|
||||
try {
|
||||
// Check permission
|
||||
if (Notification.permission === 'denied') {
|
||||
throw new Error('Notification permission was denied. Please enable notifications in your browser settings.');
|
||||
}
|
||||
|
||||
// Request permission if needed
|
||||
let permission = Notification.permission;
|
||||
if (permission === 'default') {
|
||||
permission = await Notification.requestPermission();
|
||||
}
|
||||
|
||||
if (permission !== 'granted') {
|
||||
throw new Error('Notification permission is required for push notifications.');
|
||||
}
|
||||
|
||||
console.log('PWA: Requesting push subscription...');
|
||||
|
||||
// Desktop Chrome workaround: Sometimes needs a small delay
|
||||
if (!navigator.userAgent.includes('Mobile')) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
let subscription;
|
||||
try {
|
||||
// Subscribe with shorter timeout for desktop
|
||||
const timeoutMs = navigator.userAgent.includes('Mobile') ? 15000 : 10000;
|
||||
|
||||
subscription = await Promise.race([
|
||||
this.swRegistration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Push subscription timed out after ${timeoutMs/1000} seconds.`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
|
||||
console.log('PWA: Subscription successful:', subscription.endpoint);
|
||||
|
||||
} catch (subscriptionError) {
|
||||
console.error('PWA: Subscription error:', subscriptionError);
|
||||
|
||||
// Desktop-specific error handling
|
||||
if (!navigator.userAgent.includes('Mobile')) {
|
||||
if (subscriptionError.message.includes('timeout')) {
|
||||
throw new Error('Push subscription timed out. This can happen with VPNs or corporate firewalls. The app will work without push notifications.');
|
||||
}
|
||||
}
|
||||
throw subscriptionError;
|
||||
}
|
||||
|
||||
// Send to server
|
||||
console.log('PWA: Sending subscription to server...');
|
||||
const response = await fetch('/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('p256dh')))),
|
||||
auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('auth'))))
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.pushSubscription = subscription;
|
||||
localStorage.setItem('pushSubscribed', 'true');
|
||||
console.log('PWA: Successfully subscribed to push notifications');
|
||||
this.hidePushNotificationSetup();
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('Failed to save push subscription to server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PWA: Failed to subscribe:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribeFromPushNotifications() {
|
||||
if (!this.pushSubscription) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.pushSubscription.unsubscribe();
|
||||
|
||||
await fetch('/api/push/unsubscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
endpoint: this.pushSubscription.endpoint
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
this.pushSubscription = null;
|
||||
localStorage.removeItem('pushSubscribed');
|
||||
console.log('PWA: Successfully unsubscribed from push notifications');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('PWA: Failed to unsubscribe:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
showPushNotificationSetup() {
|
||||
if (document.getElementById('push-notification-setup')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setupDiv = document.createElement('div');
|
||||
setupDiv.id = 'push-notification-setup';
|
||||
setupDiv.className = 'alert alert-info';
|
||||
setupDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
max-width: 350px;
|
||||
`;
|
||||
|
||||
setupDiv.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-bell me-2"></i>
|
||||
<div class="flex-grow-1">
|
||||
<strong>Push Notifications</strong><br>
|
||||
<small>Get notified of new orders and updates</small>
|
||||
</div>
|
||||
<button type="button" class="btn-close ms-2" id="close-push-btn" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-sm btn-primary" id="subscribe-push-btn">Enable</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ms-1" id="skip-push-btn">Not Now</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(setupDiv);
|
||||
|
||||
// Event listeners
|
||||
const subscribeBtn = document.getElementById('subscribe-push-btn');
|
||||
const skipBtn = document.getElementById('skip-push-btn');
|
||||
const closeBtn = document.getElementById('close-push-btn');
|
||||
|
||||
const hideSetup = () => {
|
||||
localStorage.setItem('pushNotificationDeclined', 'true');
|
||||
this.hidePushNotificationSetup();
|
||||
};
|
||||
|
||||
if (subscribeBtn) {
|
||||
subscribeBtn.addEventListener('click', async () => {
|
||||
subscribeBtn.disabled = true;
|
||||
skipBtn.disabled = true;
|
||||
subscribeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enabling...';
|
||||
|
||||
try {
|
||||
await this.subscribeToPushNotifications();
|
||||
|
||||
this.showNotification('Push notifications enabled!', {
|
||||
body: 'You will now receive notifications for new orders and updates.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('PWA: Subscription failed:', error);
|
||||
|
||||
let userMessage = 'Failed to enable push notifications.';
|
||||
if (error.message.includes('permission')) {
|
||||
userMessage = 'Please allow notifications when prompted.';
|
||||
} else if (error.message.includes('timeout') || error.message.includes('VPN')) {
|
||||
userMessage = 'Connection timeout. This may be due to network restrictions. The app will work without push notifications.';
|
||||
// Auto-dismiss on timeout
|
||||
hideSetup();
|
||||
alert(userMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
alert(userMessage);
|
||||
subscribeBtn.disabled = false;
|
||||
skipBtn.disabled = false;
|
||||
subscribeBtn.innerHTML = 'Enable';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (skipBtn) {
|
||||
skipBtn.addEventListener('click', hideSetup);
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', hideSetup);
|
||||
}
|
||||
}
|
||||
|
||||
hidePushNotificationSetup() {
|
||||
const setupDiv = document.getElementById('push-notification-setup');
|
||||
if (setupDiv) {
|
||||
setupDiv.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async sendTestNotification() {
|
||||
try {
|
||||
const response = await fetch('/api/push/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: 'Test Notification',
|
||||
body: 'This is a test push notification from LittleShop Admin!'
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
console.log('PWA: Test notification sent successfully');
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to send test notification');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PWA: Failed to send test notification:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize PWA Manager
|
||||
const pwaManager = new PWAManager();
|
||||
window.pwaManager = pwaManager;
|
||||
|
||||
// Expose functions globally
|
||||
window.showNotification = (title, options) => pwaManager.showNotification(title, options);
|
||||
window.sendTestPushNotification = () => pwaManager.sendTestNotification();
|
||||
window.subscribeToPushNotifications = () => pwaManager.subscribeToPushNotifications();
|
||||
window.unsubscribeFromPushNotifications = () => pwaManager.unsubscribeFromPushNotifications();
|
||||
|
||||
// Handle 401 errors globally - redirect to login
|
||||
if (window.fetch) {
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = async function(...args) {
|
||||
const response = await originalFetch.apply(this, args);
|
||||
|
||||
// Check if it's an admin area request and got 401
|
||||
if (response.status === 401 && window.location.pathname.startsWith('/Admin')) {
|
||||
// Don't redirect if already on login page
|
||||
if (!window.location.pathname.includes('/Account/Login')) {
|
||||
window.location.href = '/Admin/Account/Login?ReturnUrl=' + encodeURIComponent(window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
// Also handle 401 from direct navigation
|
||||
window.addEventListener('load', () => {
|
||||
// Check if we got redirected to /Admin instead of /Admin/Account/Login
|
||||
if (window.location.pathname === '/Admin' || window.location.pathname === '/Admin/') {
|
||||
// Check if user is authenticated by trying to fetch a protected resource
|
||||
fetch('/Admin/Dashboard', {
|
||||
method: 'HEAD',
|
||||
credentials: 'same-origin'
|
||||
}).then(response => {
|
||||
if (response.status === 401 || response.status === 302) {
|
||||
window.location.href = '/Admin/Account/Login';
|
||||
}
|
||||
}).catch(() => {
|
||||
// Network error, do nothing
|
||||
});
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user