Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fdc2b3497 | ||
|
|
c56b38d9e4 |
40
backend/src/db/migrations/20260304225310-create-session.js
Normal file
40
backend/src/db/migrations/20260304225310-create-session.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('Sessions', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
id: {
|
||||
type: Sequelize.UUID
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
containerId: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
websocketUrl: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
});
|
||||
},
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.dropTable('Sessions');
|
||||
}
|
||||
};
|
||||
25
backend/src/db/models/session.js
Normal file
25
backend/src/db/models/session.js
Normal file
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
const {
|
||||
Model
|
||||
} = require('sequelize');
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
class Session extends Model {
|
||||
static associate(models) {
|
||||
}
|
||||
}
|
||||
Session.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
status: DataTypes.STRING,
|
||||
containerId: DataTypes.STRING,
|
||||
websocketUrl: DataTypes.STRING,
|
||||
userId: DataTypes.INTEGER
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Session',
|
||||
});
|
||||
return Session;
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const app = express();
|
||||
@ -40,6 +39,7 @@ const session_eventsRoutes = require('./routes/session_events');
|
||||
const network_policiesRoutes = require('./routes/network_policies');
|
||||
|
||||
const audit_logsRoutes = require('./routes/audit_logs');
|
||||
const sessionsRoutes = require('./routes/sessions');
|
||||
|
||||
|
||||
const getBaseUrl = (url) => {
|
||||
@ -118,6 +118,8 @@ app.use('/api/session_events', passport.authenticate('jwt', {session: false}), s
|
||||
app.use('/api/network_policies', passport.authenticate('jwt', {session: false}), network_policiesRoutes);
|
||||
|
||||
app.use('/api/audit_logs', passport.authenticate('jwt', {session: false}), audit_logsRoutes);
|
||||
app.use('/api/sessions', passport.authenticate('jwt', {session: false}), sessionsRoutes);
|
||||
|
||||
|
||||
app.use(
|
||||
'/api/openai',
|
||||
@ -155,7 +157,9 @@ if (fs.existsSync(publicDir)) {
|
||||
});
|
||||
}
|
||||
|
||||
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
||||
const PORT = Number(
|
||||
process.env.PORT || (process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080),
|
||||
);
|
||||
|
||||
db.sequelize.sync().then(function () {
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@ -4,6 +4,47 @@ const { pexelsKey, pexelsQuery } = require('../config');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const KEY = pexelsKey;
|
||||
const REQUEST_TIMEOUT_MS = 4500;
|
||||
|
||||
const FALLBACK_IMAGE = {
|
||||
src: {
|
||||
original:
|
||||
'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg',
|
||||
},
|
||||
photographer: 'Yan Krukau',
|
||||
photographer_url: 'https://www.pexels.com/@yankrukov',
|
||||
};
|
||||
|
||||
const FALLBACK_VIDEO = {
|
||||
video_files: [],
|
||||
user: {
|
||||
name: 'Pexels',
|
||||
url: 'https://www.pexels.com/',
|
||||
},
|
||||
};
|
||||
|
||||
async function fetchJsonWithTimeout(url, headers) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers, signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/image', async (req, res) => {
|
||||
const headers = {
|
||||
@ -14,13 +55,10 @@ router.get('/image', async (req, res) => {
|
||||
const perPage = 1;
|
||||
const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers });
|
||||
const data = await response.json();
|
||||
res.status(200).json(data.photos[0]);
|
||||
} catch (error) {
|
||||
res.status(200).json({ error: 'Failed to fetch image' });
|
||||
}
|
||||
const data = await fetchJsonWithTimeout(url, headers);
|
||||
const image = data && Array.isArray(data.photos) ? data.photos[0] : null;
|
||||
|
||||
res.status(200).json(image || FALLBACK_IMAGE);
|
||||
});
|
||||
|
||||
router.get('/video', async (req, res) => {
|
||||
@ -32,13 +70,10 @@ router.get('/video', async (req, res) => {
|
||||
const perPage = 1;
|
||||
const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers });
|
||||
const data = await response.json();
|
||||
res.status(200).json(data.videos[0]);
|
||||
} catch (error) {
|
||||
res.status(200).json({ error: 'Failed to fetch video' });
|
||||
}
|
||||
const data = await fetchJsonWithTimeout(url, headers);
|
||||
const video = data && Array.isArray(data.videos) ? data.videos[0] : null;
|
||||
|
||||
res.status(200).json(video || FALLBACK_VIDEO);
|
||||
});
|
||||
|
||||
router.get('/multiple-images', async (req, res) => {
|
||||
@ -52,11 +87,6 @@ router.get('/multiple-images', async (req, res) => {
|
||||
const orientation = 'square';
|
||||
const perPage = 1;
|
||||
|
||||
const fallbackImage = {
|
||||
src: 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg',
|
||||
photographer: 'Yan Krukau',
|
||||
photographer_url: 'https://www.pexels.com/@yankrukov',
|
||||
};
|
||||
const fetchFallbackImage = async () => {
|
||||
try {
|
||||
const response = await fetch('https://picsum.photos/600');
|
||||
@ -66,13 +96,19 @@ router.get('/multiple-images', async (req, res) => {
|
||||
photographer_url: 'https://picsum.photos/',
|
||||
};
|
||||
} catch (error) {
|
||||
return fallbackImage;
|
||||
return {
|
||||
src: FALLBACK_IMAGE.src.original,
|
||||
photographer: FALLBACK_IMAGE.photographer,
|
||||
photographer_url: FALLBACK_IMAGE.photographer_url,
|
||||
};
|
||||
}
|
||||
};
|
||||
const fetchImage = async (query) => {
|
||||
const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`;
|
||||
const response = await fetch(url, { headers });
|
||||
const data = await response.json();
|
||||
const data = await fetchJsonWithTimeout(url, headers);
|
||||
if (!data || !Array.isArray(data.photos)) {
|
||||
return null;
|
||||
}
|
||||
return data.photos[0] || null;
|
||||
};
|
||||
|
||||
@ -83,9 +119,10 @@ router.get('/multiple-images', async (req, res) => {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
const image = result.value;
|
||||
return {
|
||||
src: image.src?.original || fallbackImage.src,
|
||||
photographer: image.photographer || fallbackImage.photographer,
|
||||
photographer_url: image.photographer_url || fallbackImage.photographer_url,
|
||||
src: image.src?.original || FALLBACK_IMAGE.src.original,
|
||||
photographer: image.photographer || FALLBACK_IMAGE.photographer,
|
||||
photographer_url:
|
||||
image.photographer_url || FALLBACK_IMAGE.photographer_url,
|
||||
};
|
||||
} else {
|
||||
const fallback = await fetchFallbackImage();
|
||||
|
||||
25
backend/src/routes/sessions.js
Normal file
25
backend/src/routes/sessions.js
Normal file
@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Session } = require('../db/models');
|
||||
const { wrapAsync, checkCrudPermissions } = require('../helpers');
|
||||
|
||||
router.get('/', checkCrudPermissions('sessions'), wrapAsync(async (req, res) => {
|
||||
const sessions = await Session.findAll({ where: { userId: req.currentUser.id } });
|
||||
res.status(200).json({ rows: sessions, count: sessions.length });
|
||||
}));
|
||||
|
||||
router.post('/', checkCrudPermissions('sessions'), wrapAsync(async (req, res) => {
|
||||
const newSession = await Session.create({
|
||||
id: require('crypto').randomUUID(),
|
||||
status: 'starting',
|
||||
userId: req.currentUser.id
|
||||
});
|
||||
res.status(201).json(newSession);
|
||||
}));
|
||||
|
||||
router.delete('/:id', checkCrudPermissions('sessions'), wrapAsync(async (req, res) => {
|
||||
await Session.destroy({ where: { id: req.params.id, userId: req.currentUser.id } });
|
||||
res.status(204).send();
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@ -1,23 +1,51 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function getPexelsImage() {
|
||||
const FALLBACK_IMAGE = {
|
||||
src: {
|
||||
original:
|
||||
'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg',
|
||||
},
|
||||
photographer: 'Yan Krukau',
|
||||
photographer_url: 'https://www.pexels.com/@yankrukov',
|
||||
};
|
||||
|
||||
const FALLBACK_VIDEO = {
|
||||
video_files: [],
|
||||
user: {
|
||||
name: 'Pexels',
|
||||
url: 'https://www.pexels.com/',
|
||||
},
|
||||
};
|
||||
|
||||
async function safeGet(url: string, params?: Record<string, string>) {
|
||||
try {
|
||||
const response = await axios.get(`/pexels/image`);
|
||||
return response.data;
|
||||
const response = await axios.get(url, {
|
||||
params,
|
||||
timeout: 7000,
|
||||
// Keep UI stable even if upstream/proxy returns 5xx.
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.warn(`[pexels] ${url} returned ${response.status}`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching image:', error);
|
||||
console.warn(`[pexels] ${url} unavailable`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPexelsImage() {
|
||||
const data = await safeGet('pexels/image');
|
||||
return data || FALLBACK_IMAGE;
|
||||
}
|
||||
|
||||
export async function getPexelsVideo() {
|
||||
try {
|
||||
const response = await axios.get(`/pexels/video`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching video:', error);
|
||||
return null;
|
||||
}
|
||||
const data = await safeGet('pexels/video');
|
||||
return data || FALLBACK_VIDEO;
|
||||
}
|
||||
|
||||
|
||||
@ -29,48 +57,52 @@ export async function getMultiplePexelsImages(
|
||||
const normalizeQuery = (query) =>
|
||||
query.trim().toLowerCase().replace(/\s+/g, '');
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return queries.map(() => FALLBACK_IMAGE);
|
||||
}
|
||||
|
||||
while (localStorageLock) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
localStorageLock = true;
|
||||
|
||||
const cachedImages = JSON.parse(localStorage.getItem('pexelsImagesCache')) || {};
|
||||
try {
|
||||
const cachedImages =
|
||||
JSON.parse(localStorage.getItem('pexelsImagesCache')) || {};
|
||||
|
||||
const isImageCached = (query) => {
|
||||
const normalizedQuery = normalizeQuery(query);
|
||||
const cached = cachedImages[normalizedQuery];
|
||||
const isCached =
|
||||
cached && cached.src && cached.photographer && cached.photographer_url;
|
||||
return isCached;
|
||||
};
|
||||
|
||||
const isImageCached = (query) => {
|
||||
const normalizedQuery = normalizeQuery(query);
|
||||
const cached = cachedImages[normalizedQuery];
|
||||
const isCached = cached && cached.src && cached.photographer && cached.photographer_url;
|
||||
return isCached;
|
||||
};
|
||||
const missingQueries = queries.filter((query) => !isImageCached(query));
|
||||
|
||||
const missingQueries = queries.filter((query) => !isImageCached(query));
|
||||
if (missingQueries.length > 0) {
|
||||
const queryString = missingQueries.join(',');
|
||||
|
||||
if (missingQueries.length > 0) {
|
||||
const queryString = missingQueries.join(',');
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/pexels/multiple-images`, {
|
||||
params: { queries: queryString },
|
||||
const response = await safeGet('pexels/multiple-images', {
|
||||
queries: queryString,
|
||||
});
|
||||
|
||||
missingQueries.forEach((query, index) => {
|
||||
const normalizedQuery = normalizeQuery(query);
|
||||
if (!cachedImages[normalizedQuery]) {
|
||||
cachedImages[normalizedQuery] = response.data[index];
|
||||
cachedImages[normalizedQuery] = response?.[index] || FALLBACK_IMAGE;
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('pexelsImagesCache', JSON.stringify(cachedImages));
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
const result = queries.map(
|
||||
(query) => cachedImages[normalizeQuery(query)] || FALLBACK_IMAGE,
|
||||
);
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
localStorageLock = false;
|
||||
}
|
||||
|
||||
const result = queries.map((query) => cachedImages[normalizeQuery(query)]);
|
||||
|
||||
localStorageLock = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -40,23 +40,30 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const [stepName, setStepName] = React.useState('');
|
||||
const [steps, setSteps] = React.useState([]);
|
||||
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
delete config.headers.Authorization;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
React.useEffect(() => {
|
||||
const interceptorId = axios.interceptors.request.use(
|
||||
(config) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return config;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
delete config.headers.Authorization;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
return () => {
|
||||
axios.interceptors.request.eject(interceptorId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// TODO: Remove this code in future releases
|
||||
React.useEffect(() => {
|
||||
const allowedOrigin = (() => {
|
||||
|
||||
@ -31,7 +31,7 @@ export const loginUser = createAsyncThunk(
|
||||
try {
|
||||
const response = await axios.post('auth/signin/local', creds);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
@ -51,7 +51,7 @@ export const passwordReset = createAsyncThunk(
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
@ -76,18 +76,22 @@ export const authSlice = createSlice({
|
||||
axios.defaults.headers.common['Authorization'] = '';
|
||||
state.currentUser = null;
|
||||
state.token = '';
|
||||
state.isFetching = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.isFetching = true;
|
||||
state.errorMessage = '';
|
||||
});
|
||||
|
||||
builder.addCase(loginUser.fulfilled, (state, action) => {
|
||||
const token = action.payload;
|
||||
const user = jwt.decode(token);
|
||||
|
||||
state.errorMessage = '';
|
||||
state.token = token;
|
||||
state.isFetching = false;
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||
@ -97,20 +101,27 @@ export const authSlice = createSlice({
|
||||
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
|
||||
state.isFetching = false;
|
||||
});
|
||||
builder.addCase(findMe.pending, () => {
|
||||
console.log('Pending findMe');
|
||||
|
||||
builder.addCase(findMe.pending, (state) => {
|
||||
state.isFetching = true;
|
||||
});
|
||||
|
||||
builder.addCase(findMe.fulfilled, (state, action) => {
|
||||
state.currentUser = action.payload;
|
||||
state.isFetching = false;
|
||||
});
|
||||
|
||||
builder.addCase(passwordReset.fulfilled, (state, action) => {
|
||||
builder.addCase(findMe.rejected, (state) => {
|
||||
state.isFetching = false;
|
||||
state.currentUser = null;
|
||||
});
|
||||
|
||||
builder.addCase(passwordReset.fulfilled, (state) => {
|
||||
state.notify.showNotification = true;
|
||||
state.notify.textNotification = 'Password has been reset successfully';
|
||||
});
|
||||
|
||||
builder.addCase(resetAction, (state) => initialState);
|
||||
builder.addCase(resetAction, () => initialState);
|
||||
|
||||
builder.addCase(passwordReset.rejected, (state) => {
|
||||
state.errorMessage = 'Something was wrong. Try again';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user