Fix login button and improve CORS
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
- Fixed login URL from /auth/steam to /api/auth/steam - Updated all Steam login buttons to custom green design with 'Login to Steam' text - Enhanced CORS configuration with explicit preflight handling - Added Steam image proxy endpoint for CORS-free image loading - Improved environment variable management with .env.local support - Added ENV_SETUP.md guide for environment configuration
This commit is contained in:
2
.env
2
.env
@@ -5,7 +5,7 @@ HOST=0.0.0.0
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://turbotrades:20yBBj!0@localhost:27017/turbotrades?authSource=admin
|
||||
|
||||
#MONGODB_URI=mongodb://localhost:27017/turbotrades
|
||||
# Session
|
||||
SESSION_SECRET=5b4051d25c348e06ffebf77bc66fcb8db9c5b32f58eb8c6fac6c937b666fc733
|
||||
|
||||
|
||||
214
ENV_SETUP.md
Normal file
214
ENV_SETUP.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Environment Variables Setup Guide
|
||||
|
||||
This guide explains how to manage environment variables for local development vs. production deployment.
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
TurboTrades/
|
||||
├── .env # Production/deployment settings (COMMITTED to git)
|
||||
├── .env.local # Local development overrides (NOT committed)
|
||||
├── .env.example # Template for reference
|
||||
└── .env.tunnel # Cloudflare tunnel settings (optional)
|
||||
```
|
||||
|
||||
## 🔄 How It Works
|
||||
|
||||
The application loads environment variables in this order:
|
||||
|
||||
1. **`.env.local`** - Loaded first (highest priority)
|
||||
2. **`.env`** - Loaded second (fallback values)
|
||||
|
||||
This means:
|
||||
- Variables in `.env.local` **override** those in `.env`
|
||||
- `.env.local` is in `.gitignore` and **never gets pushed** to git
|
||||
- `.env` contains your production settings and **is committed** to git
|
||||
|
||||
## 🚀 Quick Setup
|
||||
|
||||
### For Local Development
|
||||
|
||||
1. **Copy the template:**
|
||||
```bash
|
||||
# .env.local is already created for you!
|
||||
# Just add your Steam API key
|
||||
```
|
||||
|
||||
2. **Add your Steam API Key to `.env.local`:**
|
||||
```env
|
||||
STEAM_API_KEY=your_actual_steam_api_key_here
|
||||
```
|
||||
|
||||
Get your key from: https://steamcommunity.com/dev/apikey
|
||||
|
||||
3. **Start the server:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The server will automatically use:
|
||||
- Local MongoDB (`mongodb://localhost:27017/turbotrades`)
|
||||
- Local frontend (`http://localhost:5173`)
|
||||
- No HTTPS required (`COOKIE_SECURE=false`)
|
||||
|
||||
### For Production/Deployment
|
||||
|
||||
Your `.env` file should contain production settings:
|
||||
- Production MongoDB connection (MongoDB Atlas or remote server)
|
||||
- Production domain for CORS
|
||||
- Secure cookies enabled (`COOKIE_SECURE=true`)
|
||||
- Production Steam return URL
|
||||
|
||||
**Never commit sensitive production credentials!** Use environment variables in your hosting platform instead.
|
||||
|
||||
## 🔧 Common Scenarios
|
||||
|
||||
### Scenario 1: Switching Between Local and Production MongoDB
|
||||
|
||||
**Problem:** You keep changing `MONGODB_URI` in `.env` and forgetting to change it back.
|
||||
|
||||
**Solution:**
|
||||
- Keep production MongoDB URI in `.env`
|
||||
- Put local MongoDB URI in `.env.local`
|
||||
- Never edit `.env` for local development!
|
||||
|
||||
```env
|
||||
# .env (production - committed)
|
||||
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/turbotrades
|
||||
|
||||
# .env.local (local dev - not committed)
|
||||
MONGODB_URI=mongodb://localhost:27017/turbotrades
|
||||
```
|
||||
|
||||
### Scenario 2: Testing with Cloudflare Tunnel
|
||||
|
||||
Use `.env.tunnel` for tunnel-specific settings:
|
||||
|
||||
```bash
|
||||
# Copy tunnel settings
|
||||
cp .env.tunnel .env.local
|
||||
|
||||
# Or manually edit .env.local with your tunnel URL
|
||||
```
|
||||
|
||||
### Scenario 3: Team Development
|
||||
|
||||
**Each developer should:**
|
||||
1. Keep their own `.env.local` (never commit it!)
|
||||
2. Share the `.env` file with production-like defaults
|
||||
3. Update `.env.example` when adding new variables
|
||||
|
||||
## 📝 Environment Variables Reference
|
||||
|
||||
### Database
|
||||
```env
|
||||
MONGODB_URI=mongodb://localhost:27017/turbotrades
|
||||
```
|
||||
|
||||
### Server
|
||||
```env
|
||||
NODE_ENV=development # or 'production'
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
```
|
||||
|
||||
### Steam OAuth
|
||||
```env
|
||||
STEAM_API_KEY=your_key_here
|
||||
STEAM_REALM=http://localhost:3000
|
||||
STEAM_RETURN_URL=http://localhost:3000/api/auth/steam/return
|
||||
```
|
||||
|
||||
### CORS & Cookies
|
||||
```env
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
COOKIE_DOMAIN=localhost
|
||||
COOKIE_SECURE=false # true for production
|
||||
COOKIE_SAME_SITE=lax
|
||||
```
|
||||
|
||||
### JWT & Sessions
|
||||
```env
|
||||
JWT_ACCESS_SECRET=your-secret-here
|
||||
JWT_REFRESH_SECRET=your-secret-here
|
||||
JWT_ACCESS_EXPIRY=15m
|
||||
JWT_REFRESH_EXPIRY=7d
|
||||
SESSION_SECRET=your-session-secret
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
```env
|
||||
RATE_LIMIT_MAX=100
|
||||
RATE_LIMIT_TIMEWINDOW=60000
|
||||
```
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
### ✅ DO:
|
||||
- Use `.env.local` for local development
|
||||
- Commit `.env` with safe default values
|
||||
- Keep sensitive keys in `.env.local`
|
||||
- Update `.env.example` when adding new variables
|
||||
- Use your hosting platform's environment variables for production secrets
|
||||
|
||||
### ❌ DON'T:
|
||||
- Commit `.env.local` to git (it's in `.gitignore`)
|
||||
- Store real API keys in `.env` (use placeholders)
|
||||
- Edit `.env` for local development
|
||||
- Share your `.env.local` file with others
|
||||
- Commit sensitive credentials to the repository
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### "Authentication failed" MongoDB Error
|
||||
**Cause:** Using production MongoDB URI with credentials locally.
|
||||
|
||||
**Fix:** Make sure `.env.local` has:
|
||||
```env
|
||||
MONGODB_URI=mongodb://localhost:27017/turbotrades
|
||||
```
|
||||
|
||||
### "CORS Error" in Browser
|
||||
**Cause:** Backend and frontend origins don't match.
|
||||
|
||||
**Fix:** Check `.env.local`:
|
||||
```env
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
```
|
||||
|
||||
### "Steam authentication failed"
|
||||
**Cause:** Missing or invalid Steam API key.
|
||||
|
||||
**Fix:** Add your key to `.env.local`:
|
||||
```env
|
||||
STEAM_API_KEY=your_actual_key_from_steam
|
||||
```
|
||||
Get it here: https://steamcommunity.com/dev/apikey
|
||||
|
||||
### Changes not taking effect
|
||||
**Cause:** Server not restarting or wrong file being edited.
|
||||
|
||||
**Fix:**
|
||||
1. Make sure you're editing `.env.local` (not `.env`)
|
||||
2. Restart the server: `npm run dev`
|
||||
3. Clear `node_modules/.cache` if needed
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [dotenv documentation](https://github.com/motdotla/dotenv)
|
||||
- [Steam Web API documentation](https://steamcommunity.com/dev)
|
||||
- [MongoDB Connection Strings](https://www.mongodb.com/docs/manual/reference/connection-string/)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
When adding new environment variables:
|
||||
|
||||
1. Add it to `config/index.js` with a sensible default
|
||||
2. Add it to `.env.example` with a placeholder value
|
||||
3. Document it in this file
|
||||
4. Update `API_ENDPOINTS.md` if it affects API behavior
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2024
|
||||
**Maintainer:** TurboTrades Development Team
|
||||
@@ -1,6 +1,13 @@
|
||||
import dotenv from "dotenv";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
dotenv.config();
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load .env.local first (for local development overrides), then .env
|
||||
dotenv.config({ path: join(__dirname, "..", ".env.local") });
|
||||
dotenv.config({ path: join(__dirname, "..", ".env") });
|
||||
|
||||
export const config = {
|
||||
// Server
|
||||
|
||||
@@ -250,12 +250,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button v-else @click="handleLogin" class="btn btn-primary">
|
||||
<img
|
||||
src="https://community.cloudflare.steamstatic.com/public/images/signinthroughsteam/sits_01.png"
|
||||
alt="Sign in through Steam"
|
||||
class="h-6"
|
||||
/>
|
||||
<button
|
||||
v-else
|
||||
@click="handleLogin"
|
||||
class="px-6 py-2.5 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
|
||||
>
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2a10 10 0 0 0-10 10 10 10 0 0 0 10 10c.5 0 1-.04 1.48-.1L8.44 14.3a3.2 3.2 0 0 1-.94-2.27c0-1.77 1.43-3.2 3.2-3.2.53 0 1.03.13 1.47.36L15.5 6.7A9.96 9.96 0 0 0 12 2m6.5 4.5-4.67 2.13c.52.47.84 1.15.84 1.9 0 1.41-1.15 2.57-2.57 2.57-.17 0-.33-.02-.49-.05l-2.25 3.25.02.02c2.25.55 4.77-.51 6.41-2.8 2.02-2.83 1.78-6.7-.29-7.02z"
|
||||
/>
|
||||
</svg>
|
||||
Login to Steam
|
||||
</button>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
|
||||
@@ -1,223 +1,238 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import axios from "axios";
|
||||
import { useToast } from "vue-toastification";
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useToast();
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
// State
|
||||
const user = ref(null)
|
||||
const isAuthenticated = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isInitialized = ref(false)
|
||||
const user = ref(null);
|
||||
const isAuthenticated = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isInitialized = ref(false);
|
||||
|
||||
// Computed
|
||||
const username = computed(() => user.value?.username || 'Guest')
|
||||
const steamId = computed(() => user.value?.steamId || null)
|
||||
const avatar = computed(() => user.value?.avatar || null)
|
||||
const balance = computed(() => user.value?.balance || 0)
|
||||
const staffLevel = computed(() => user.value?.staffLevel || 0)
|
||||
const isStaff = computed(() => staffLevel.value > 0)
|
||||
const isModerator = computed(() => staffLevel.value >= 2)
|
||||
const isAdmin = computed(() => staffLevel.value >= 3)
|
||||
const tradeUrl = computed(() => user.value?.tradeUrl || null)
|
||||
const email = computed(() => user.value?.email?.address || null)
|
||||
const emailVerified = computed(() => user.value?.email?.verified || false)
|
||||
const isBanned = computed(() => user.value?.ban?.banned || false)
|
||||
const banReason = computed(() => user.value?.ban?.reason || null)
|
||||
const twoFactorEnabled = computed(() => user.value?.twoFactor?.enabled || false)
|
||||
const username = computed(() => user.value?.username || "Guest");
|
||||
const steamId = computed(() => user.value?.steamId || null);
|
||||
const avatar = computed(() => user.value?.avatar || null);
|
||||
const balance = computed(() => user.value?.balance || 0);
|
||||
const staffLevel = computed(() => user.value?.staffLevel || 0);
|
||||
const isStaff = computed(() => staffLevel.value > 0);
|
||||
const isModerator = computed(() => staffLevel.value >= 2);
|
||||
const isAdmin = computed(() => staffLevel.value >= 3);
|
||||
const tradeUrl = computed(() => user.value?.tradeUrl || null);
|
||||
const email = computed(() => user.value?.email?.address || null);
|
||||
const emailVerified = computed(() => user.value?.email?.verified || false);
|
||||
const isBanned = computed(() => user.value?.ban?.banned || false);
|
||||
const banReason = computed(() => user.value?.ban?.reason || null);
|
||||
const twoFactorEnabled = computed(
|
||||
() => user.value?.twoFactor?.enabled || false
|
||||
);
|
||||
|
||||
// Actions
|
||||
const setUser = (userData) => {
|
||||
user.value = userData
|
||||
isAuthenticated.value = !!userData
|
||||
}
|
||||
user.value = userData;
|
||||
isAuthenticated.value = !!userData;
|
||||
};
|
||||
|
||||
const clearUser = () => {
|
||||
user.value = null
|
||||
isAuthenticated.value = false
|
||||
}
|
||||
user.value = null;
|
||||
isAuthenticated.value = false;
|
||||
};
|
||||
|
||||
const fetchUser = async () => {
|
||||
if (isLoading.value) return
|
||||
if (isLoading.value) return;
|
||||
|
||||
isLoading.value = true
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await axios.get('/api/auth/me', {
|
||||
const response = await axios.get("/api/auth/me", {
|
||||
withCredentials: true,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.user) {
|
||||
setUser(response.data.user)
|
||||
return response.data.user
|
||||
setUser(response.data.user);
|
||||
return response.data.user;
|
||||
} else {
|
||||
clearUser()
|
||||
return null
|
||||
clearUser();
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user:', error)
|
||||
clearUser()
|
||||
return null
|
||||
console.error("Failed to fetch user:", error);
|
||||
clearUser();
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isInitialized.value = true
|
||||
isLoading.value = false;
|
||||
isInitialized.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const login = () => {
|
||||
// Redirect to Steam login
|
||||
window.location.href = '/api/auth/steam'
|
||||
}
|
||||
// Redirect to Steam login on backend API domain
|
||||
const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:3000";
|
||||
window.location.href = `${apiUrl}/api/auth/steam`;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
isLoading.value = true
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await axios.post('/api/auth/logout', {}, {
|
||||
withCredentials: true,
|
||||
})
|
||||
await axios.post(
|
||||
"/api/auth/logout",
|
||||
{},
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
|
||||
clearUser()
|
||||
toast.success('Successfully logged out')
|
||||
clearUser();
|
||||
toast.success("Successfully logged out");
|
||||
|
||||
// Redirect to home page
|
||||
if (window.location.pathname !== '/') {
|
||||
window.location.href = '/'
|
||||
if (window.location.pathname !== "/") {
|
||||
window.location.href = "/";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
toast.error('Failed to logout')
|
||||
console.error("Logout error:", error);
|
||||
toast.error("Failed to logout");
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshToken = async () => {
|
||||
try {
|
||||
await axios.post('/api/auth/refresh', {}, {
|
||||
withCredentials: true,
|
||||
})
|
||||
return true
|
||||
await axios.post(
|
||||
"/api/auth/refresh",
|
||||
{},
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error)
|
||||
clearUser()
|
||||
return false
|
||||
console.error("Token refresh failed:", error);
|
||||
clearUser();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateTradeUrl = async (tradeUrl) => {
|
||||
isLoading.value = true
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await axios.patch('/api/user/trade-url',
|
||||
const response = await axios.patch(
|
||||
"/api/user/trade-url",
|
||||
{ tradeUrl },
|
||||
{ withCredentials: true }
|
||||
)
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
user.value.tradeUrl = tradeUrl
|
||||
toast.success('Trade URL updated successfully')
|
||||
return true
|
||||
user.value.tradeUrl = tradeUrl;
|
||||
toast.success("Trade URL updated successfully");
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to update trade URL:', error)
|
||||
toast.error(error.response?.data?.message || 'Failed to update trade URL')
|
||||
return false
|
||||
console.error("Failed to update trade URL:", error);
|
||||
toast.error(
|
||||
error.response?.data?.message || "Failed to update trade URL"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateEmail = async (email) => {
|
||||
isLoading.value = true
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await axios.patch('/api/user/email',
|
||||
const response = await axios.patch(
|
||||
"/api/user/email",
|
||||
{ email },
|
||||
{ withCredentials: true }
|
||||
)
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
user.value.email = { address: email, verified: false }
|
||||
toast.success('Email updated! Check your inbox for verification link')
|
||||
return true
|
||||
user.value.email = { address: email, verified: false };
|
||||
toast.success("Email updated! Check your inbox for verification link");
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to update email:', error)
|
||||
toast.error(error.response?.data?.message || 'Failed to update email')
|
||||
return false
|
||||
console.error("Failed to update email:", error);
|
||||
toast.error(error.response?.data?.message || "Failed to update email");
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const verifyEmail = async (token) => {
|
||||
isLoading.value = true
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await axios.get(`/api/user/verify-email/${token}`, {
|
||||
withCredentials: true
|
||||
})
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success('Email verified successfully!')
|
||||
await fetchUser() // Refresh user data
|
||||
return true
|
||||
toast.success("Email verified successfully!");
|
||||
await fetchUser(); // Refresh user data
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to verify email:', error)
|
||||
toast.error(error.response?.data?.message || 'Failed to verify email')
|
||||
return false
|
||||
console.error("Failed to verify email:", error);
|
||||
toast.error(error.response?.data?.message || "Failed to verify email");
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getUserStats = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/user/stats', {
|
||||
withCredentials: true
|
||||
})
|
||||
const response = await axios.get("/api/user/stats", {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.stats
|
||||
return response.data.stats;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user stats:', error)
|
||||
return null
|
||||
console.error("Failed to fetch user stats:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getBalance = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/user/balance', {
|
||||
withCredentials: true
|
||||
})
|
||||
const response = await axios.get("/api/user/balance", {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
user.value.balance = response.data.balance
|
||||
return response.data.balance
|
||||
user.value.balance = response.data.balance;
|
||||
return response.data.balance;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch balance:', error)
|
||||
return null
|
||||
console.error("Failed to fetch balance:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateBalance = (newBalance) => {
|
||||
if (user.value) {
|
||||
user.value.balance = newBalance
|
||||
user.value.balance = newBalance;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on store creation
|
||||
const initialize = async () => {
|
||||
if (!isInitialized.value) {
|
||||
await fetchUser()
|
||||
await fetchUser();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
@@ -256,5 +271,5 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
getBalance,
|
||||
updateBalance,
|
||||
initialize,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@@ -381,13 +381,14 @@ const getRarityColor = (rarity) => {
|
||||
<button
|
||||
v-if="!authStore.isAuthenticated"
|
||||
@click="authStore.login"
|
||||
class="btn btn-lg bg-white text-primary-600 hover:bg-gray-100"
|
||||
class="px-8 py-3.5 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-3 text-lg"
|
||||
>
|
||||
<img
|
||||
src="https://community.cloudflare.steamstatic.com/public/images/signinthroughsteam/sits_01.png"
|
||||
alt="Sign in through Steam"
|
||||
class="h-6"
|
||||
/>
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2a10 10 0 0 0-10 10 10 10 0 0 0 10 10c.5 0 1-.04 1.48-.1L8.44 14.3a3.2 3.2 0 0 1-.94-2.27c0-1.77 1.43-3.2 3.2-3.2.53 0 1.03.13 1.47.36L15.5 6.7A9.96 9.96 0 0 0 12 2m6.5 4.5-4.67 2.13c.52.47.84 1.15.84 1.9 0 1.41-1.15 2.57-2.57 2.57-.17 0-.33-.02-.49-.05l-2.25 3.25.02.02c2.25.55 4.77-.51 6.41-2.8 2.02-2.83 1.78-6.7-.29-7.02z"
|
||||
/>
|
||||
</svg>
|
||||
Login to Steam
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
<div class="login-section">
|
||||
<p class="login-prompt">Admin? Login to access the site</p>
|
||||
<a :href="steamLoginUrl" class="steam-login-btn">
|
||||
<svg class="steam-icon" viewBox="0 0 256 256" fill="currentColor">
|
||||
<svg class="steam-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M127.999 0C57.421 0 0 57.421 0 127.999c0 63.646 46.546 116.392 107.404 126.284l35.542-51.937c-2.771.413-5.623.631-8.525.631-29.099 0-52.709-23.611-52.709-52.709 0-29.099 23.61-52.709 52.709-52.709 29.098 0 52.708 23.61 52.708 52.709 0 2.902-.218 5.754-.631 8.525l51.937 35.542C248.423 173.536 256 151.997 256 127.999 256 57.421 198.579 0 127.999 0zm-1.369 96.108c-20.175 0-36.559 16.383-36.559 36.559 0 .367.006.732.018 1.096l24.357 10.07c4.023-2.503 8.772-3.951 13.844-3.951 14.576 0 26.418 11.842 26.418 26.418s-11.842 26.418-26.418 26.418c-14.048 0-25.538-10.997-26.343-24.853l-23.554-9.742c.04.832.061 1.669.061 2.51 0 20.175 16.383 36.559 36.559 36.559 20.175 0 36.558-16.384 36.558-36.559 0-20.176-16.383-36.559-36.558-36.559z"
|
||||
d="M12 2a10 10 0 0 0-10 10 10 10 0 0 0 10 10c.5 0 1-.04 1.48-.1L8.44 14.3a3.2 3.2 0 0 1-.94-2.27c0-1.77 1.43-3.2 3.2-3.2.53 0 1.03.13 1.47.36L15.5 6.7A9.96 9.96 0 0 0 12 2m6.5 4.5-4.67 2.13c.52.47.84 1.15.84 1.9 0 1.41-1.15 2.57-2.57 2.57-.17 0-.33-.02-.49-.05l-2.25 3.25.02.02c2.25.55 4.77-.51 6.41-2.8 2.02-2.83 1.78-6.7-.29-7.02z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Login with Steam</span>
|
||||
<span>Login to Steam</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -423,7 +423,7 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 2rem;
|
||||
background: linear-gradient(135deg, #171a21 0%, #1b2838 100%);
|
||||
background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
@@ -435,7 +435,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.steam-login-btn:hover {
|
||||
background: linear-gradient(135deg, #1b2838 0%, #2a475e 100%);
|
||||
background: linear-gradient(135deg, #15803d 0%, #166534 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
|
||||
16
index.js
16
index.js
@@ -91,9 +91,19 @@ const registerPlugins = async (fastify) => {
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
|
||||
exposedHeaders: ["Set-Cookie"],
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
||||
allowedHeaders: [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
"Cookie",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
],
|
||||
exposedHeaders: ["Set-Cookie", "Content-Type"],
|
||||
preflight: true,
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204,
|
||||
maxAge: 86400, // Cache preflight requests for 24 hours
|
||||
});
|
||||
|
||||
// Security headers
|
||||
|
||||
@@ -68,6 +68,71 @@ export default async function authRoutes(fastify, options) {
|
||||
});
|
||||
});
|
||||
|
||||
// Steam image proxy - proxy Steam CDN images to avoid CORS issues
|
||||
fastify.get("/steam/image-proxy", async (request, reply) => {
|
||||
try {
|
||||
const { url } = request.query;
|
||||
|
||||
if (!url) {
|
||||
return reply.status(400).send({
|
||||
error: "BadRequest",
|
||||
message: "Image URL is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that it's a Steam CDN URL
|
||||
const validDomains = [
|
||||
"community.steamstatic.com",
|
||||
"community.cloudflare.steamstatic.com",
|
||||
"cdn.steamstatic.com",
|
||||
"cdn.cloudflare.steamstatic.com",
|
||||
"avatars.steamstatic.com",
|
||||
"avatars.cloudflare.steamstatic.com",
|
||||
];
|
||||
|
||||
const urlObj = new URL(url);
|
||||
const isValidDomain = validDomains.some(
|
||||
(domain) => urlObj.hostname === domain
|
||||
);
|
||||
|
||||
if (!isValidDomain) {
|
||||
return reply.status(400).send({
|
||||
error: "BadRequest",
|
||||
message: "Invalid Steam CDN URL",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the image from Steam
|
||||
const imageResponse = await fetch(url);
|
||||
|
||||
if (!imageResponse.ok) {
|
||||
return reply.status(imageResponse.status).send({
|
||||
error: "FetchError",
|
||||
message: "Failed to fetch image from Steam CDN",
|
||||
});
|
||||
}
|
||||
|
||||
// Get the image buffer
|
||||
const imageBuffer = await imageResponse.arrayBuffer();
|
||||
const contentType =
|
||||
imageResponse.headers.get("content-type") || "image/jpeg";
|
||||
|
||||
// Set appropriate headers
|
||||
reply.header("Content-Type", contentType);
|
||||
reply.header("Cache-Control", "public, max-age=86400"); // Cache for 24 hours
|
||||
reply.header("Access-Control-Allow-Origin", config.cors.origin);
|
||||
|
||||
return reply.send(Buffer.from(imageBuffer));
|
||||
} catch (error) {
|
||||
console.error("❌ Steam image proxy error:", error);
|
||||
return reply.status(500).send({
|
||||
error: "ProxyError",
|
||||
message: "Failed to proxy Steam image",
|
||||
details: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Steam login - initiate OAuth flow
|
||||
fastify.get("/steam", async (request, reply) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user