Compare commits

...

2 Commits

Author SHA1 Message Date
Flatlogic Bot
2fdc2b3497 Oo 2026-03-04 23:10:55 +00:00
Flatlogic Bot
c56b38d9e4 Tes 2026-03-04 23:01:37 +00:00
8 changed files with 264 additions and 83 deletions

View 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');
}
};

View 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;
};

View File

@ -1,4 +1,3 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const app = express(); const app = express();
@ -40,6 +39,7 @@ const session_eventsRoutes = require('./routes/session_events');
const network_policiesRoutes = require('./routes/network_policies'); const network_policiesRoutes = require('./routes/network_policies');
const audit_logsRoutes = require('./routes/audit_logs'); const audit_logsRoutes = require('./routes/audit_logs');
const sessionsRoutes = require('./routes/sessions');
const getBaseUrl = (url) => { 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/network_policies', passport.authenticate('jwt', {session: false}), network_policiesRoutes);
app.use('/api/audit_logs', passport.authenticate('jwt', {session: false}), audit_logsRoutes); app.use('/api/audit_logs', passport.authenticate('jwt', {session: false}), audit_logsRoutes);
app.use('/api/sessions', passport.authenticate('jwt', {session: false}), sessionsRoutes);
app.use( app.use(
'/api/openai', '/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 () { db.sequelize.sync().then(function () {
app.listen(PORT, () => { app.listen(PORT, () => {

View File

@ -4,6 +4,47 @@ const { pexelsKey, pexelsQuery } = require('../config');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const KEY = pexelsKey; 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) => { router.get('/image', async (req, res) => {
const headers = { const headers = {
@ -14,13 +55,10 @@ router.get('/image', async (req, res) => {
const perPage = 1; const perPage = 1;
const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`;
try { const data = await fetchJsonWithTimeout(url, headers);
const response = await fetch(url, { headers }); const image = data && Array.isArray(data.photos) ? data.photos[0] : null;
const data = await response.json();
res.status(200).json(data.photos[0]); res.status(200).json(image || FALLBACK_IMAGE);
} catch (error) {
res.status(200).json({ error: 'Failed to fetch image' });
}
}); });
router.get('/video', async (req, res) => { router.get('/video', async (req, res) => {
@ -32,13 +70,10 @@ router.get('/video', async (req, res) => {
const perPage = 1; const perPage = 1;
const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`;
try { const data = await fetchJsonWithTimeout(url, headers);
const response = await fetch(url, { headers }); const video = data && Array.isArray(data.videos) ? data.videos[0] : null;
const data = await response.json();
res.status(200).json(data.videos[0]); res.status(200).json(video || FALLBACK_VIDEO);
} catch (error) {
res.status(200).json({ error: 'Failed to fetch video' });
}
}); });
router.get('/multiple-images', async (req, res) => { router.get('/multiple-images', async (req, res) => {
@ -52,11 +87,6 @@ router.get('/multiple-images', async (req, res) => {
const orientation = 'square'; const orientation = 'square';
const perPage = 1; 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 () => { const fetchFallbackImage = async () => {
try { try {
const response = await fetch('https://picsum.photos/600'); const response = await fetch('https://picsum.photos/600');
@ -66,13 +96,19 @@ router.get('/multiple-images', async (req, res) => {
photographer_url: 'https://picsum.photos/', photographer_url: 'https://picsum.photos/',
}; };
} catch (error) { } 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 fetchImage = async (query) => {
const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; 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 fetchJsonWithTimeout(url, headers);
const data = await response.json(); if (!data || !Array.isArray(data.photos)) {
return null;
}
return data.photos[0] || null; return data.photos[0] || null;
}; };
@ -83,9 +119,10 @@ router.get('/multiple-images', async (req, res) => {
if (result.status === 'fulfilled' && result.value) { if (result.status === 'fulfilled' && result.value) {
const image = result.value; const image = result.value;
return { return {
src: image.src?.original || fallbackImage.src, src: image.src?.original || FALLBACK_IMAGE.src.original,
photographer: image.photographer || fallbackImage.photographer, photographer: image.photographer || FALLBACK_IMAGE.photographer,
photographer_url: image.photographer_url || fallbackImage.photographer_url, photographer_url:
image.photographer_url || FALLBACK_IMAGE.photographer_url,
}; };
} else { } else {
const fallback = await fetchFallbackImage(); const fallback = await fetchFallbackImage();

View 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;

View File

@ -1,23 +1,51 @@
import axios from 'axios'; 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 { try {
const response = await axios.get(`/pexels/image`); const response = await axios.get(url, {
return response.data; 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) { } catch (error) {
console.error('Error fetching image:', error); console.warn(`[pexels] ${url} unavailable`);
return null; return null;
} }
} }
export async function getPexelsImage() {
const data = await safeGet('pexels/image');
return data || FALLBACK_IMAGE;
}
export async function getPexelsVideo() { export async function getPexelsVideo() {
try { const data = await safeGet('pexels/video');
const response = await axios.get(`/pexels/video`); return data || FALLBACK_VIDEO;
return response.data;
} catch (error) {
console.error('Error fetching video:', error);
return null;
}
} }
@ -29,48 +57,52 @@ export async function getMultiplePexelsImages(
const normalizeQuery = (query) => const normalizeQuery = (query) =>
query.trim().toLowerCase().replace(/\s+/g, ''); query.trim().toLowerCase().replace(/\s+/g, '');
if (typeof window === 'undefined') {
return queries.map(() => FALLBACK_IMAGE);
}
while (localStorageLock) { while (localStorageLock) {
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
} }
localStorageLock = true; 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 missingQueries = queries.filter((query) => !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)); if (missingQueries.length > 0) {
const queryString = missingQueries.join(',');
if (missingQueries.length > 0) { const response = await safeGet('pexels/multiple-images', {
const queryString = missingQueries.join(','); queries: queryString,
try {
const response = await axios.get(`/pexels/multiple-images`, {
params: { queries: queryString },
}); });
missingQueries.forEach((query, index) => { missingQueries.forEach((query, index) => {
const normalizedQuery = normalizeQuery(query); const normalizedQuery = normalizeQuery(query);
if (!cachedImages[normalizedQuery]) { if (!cachedImages[normalizedQuery]) {
cachedImages[normalizedQuery] = response.data[index]; cachedImages[normalizedQuery] = response?.[index] || FALLBACK_IMAGE;
} }
}); });
localStorage.setItem('pexelsImagesCache', JSON.stringify(cachedImages)); 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;
} }

View File

@ -40,23 +40,30 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const [stepName, setStepName] = React.useState(''); const [stepName, setStepName] = React.useState('');
const [steps, setSteps] = React.useState([]); const [steps, setSteps] = React.useState([]);
axios.interceptors.request.use( React.useEffect(() => {
config => { const interceptorId = axios.interceptors.request.use(
const token = localStorage.getItem('token'); (config) => {
if (typeof window === 'undefined') {
if (token) { return config;
config.headers.Authorization = `Bearer ${token}`;
} else {
delete config.headers.Authorization;
}
return config;
},
error => {
return Promise.reject(error);
} }
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 // TODO: Remove this code in future releases
React.useEffect(() => { React.useEffect(() => {
const allowedOrigin = (() => { const allowedOrigin = (() => {

View File

@ -31,7 +31,7 @@ export const loginUser = createAsyncThunk(
try { try {
const response = await axios.post('auth/signin/local', creds); const response = await axios.post('auth/signin/local', creds);
return response.data; return response.data;
} catch (error) { } catch (error: any) {
if (!error.response) { if (!error.response) {
throw error; throw error;
} }
@ -51,7 +51,7 @@ export const passwordReset = createAsyncThunk(
}); });
return response.data; return response.data;
} catch (error) { } catch (error: any) {
if (!error.response) { if (!error.response) {
throw error; throw error;
} }
@ -76,18 +76,22 @@ export const authSlice = createSlice({
axios.defaults.headers.common['Authorization'] = ''; axios.defaults.headers.common['Authorization'] = '';
state.currentUser = null; state.currentUser = null;
state.token = ''; state.token = '';
state.isFetching = false;
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => { builder.addCase(loginUser.pending, (state) => {
state.isFetching = true; state.isFetching = true;
state.errorMessage = '';
}); });
builder.addCase(loginUser.fulfilled, (state, action) => { builder.addCase(loginUser.fulfilled, (state, action) => {
const token = action.payload; const token = action.payload;
const user = jwt.decode(token); const user = jwt.decode(token);
state.errorMessage = ''; state.errorMessage = '';
state.token = token; state.token = token;
state.isFetching = false;
localStorage.setItem('token', token); localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user)); localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; 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.errorMessage = String(action.payload) || 'Something went wrong. Try again';
state.isFetching = false; 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) => { builder.addCase(findMe.fulfilled, (state, action) => {
state.currentUser = action.payload; state.currentUser = action.payload;
state.isFetching = false; 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.showNotification = true;
state.notify.textNotification = 'Password has been reset successfully'; state.notify.textNotification = 'Password has been reset successfully';
}); });
builder.addCase(resetAction, (state) => initialState); builder.addCase(resetAction, () => initialState);
builder.addCase(passwordReset.rejected, (state) => { builder.addCase(passwordReset.rejected, (state) => {
state.errorMessage = 'Something was wrong. Try again'; state.errorMessage = 'Something was wrong. Try again';