Compare commits

...

13 Commits

Author SHA1 Message Date
Flatlogic Bot
c1ff6b0871 12 2026-03-02 02:14:51 +00:00
Flatlogic Bot
2c36af7989 11 2026-03-02 00:52:09 +00:00
Flatlogic Bot
9bcf455037 10 2026-03-02 00:26:20 +00:00
Flatlogic Bot
0ccc8a095c 8 2026-03-01 23:58:06 +00:00
Flatlogic Bot
92c72bfcd0 7 2026-03-01 23:25:55 +00:00
Flatlogic Bot
eb5f060d78 8 2026-03-01 22:47:03 +00:00
Flatlogic Bot
26016a68c3 7 2026-03-01 22:30:45 +00:00
Flatlogic Bot
a1d15263db 6 2026-03-01 22:10:45 +00:00
Flatlogic Bot
7c8a152858 5 2026-03-01 21:58:02 +00:00
Flatlogic Bot
1fe82c2536 4 2026-03-01 21:23:57 +00:00
Flatlogic Bot
0adfe0fe78 3 2026-03-01 21:15:03 +00:00
Flatlogic Bot
e1f7182cfc 2 2026-03-01 20:56:09 +00:00
Flatlogic Bot
14794ed687 1 2026-03-01 20:26:04 +00:00
24 changed files with 1690 additions and 1119 deletions

View File

@ -1,6 +1,3 @@
const os = require('os'); const os = require('os');
const config = { const config = {
@ -14,6 +11,7 @@ const config = {
admin_pass: "8e470127", admin_pass: "8e470127",
user_pass: "f04a8902244c", user_pass: "f04a8902244c",
admin_email: "admin@flatlogic.com", admin_email: "admin@flatlogic.com",
admin_private_key: process.env.ADMIN_PRIVATE_KEY || 'studio-admin-key-9283-7465-1029',
providers: { providers: {
LOCAL: 'local', LOCAL: 'local',
GOOGLE: 'google', GOOGLE: 'google',

View File

@ -1,4 +1,3 @@
const db = require('../models'); const db = require('../models');
const FileDBApi = require('./file'); const FileDBApi = require('./file');
const crypto = require('crypto'); const crypto = require('crypto');
@ -61,6 +60,8 @@ module.exports = class Ai_song_requestsDBApi {
null null
, ,
ai_data: data.ai_data || null,
importHash: data.importHash || null, importHash: data.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
updatedById: currentUser.id, updatedById: currentUser.id,
@ -89,7 +90,6 @@ module.exports = class Ai_song_requestsDBApi {
return ai_song_requests; return ai_song_requests;
} }
static async bulkImport(data, options) { static async bulkImport(data, options) {
const currentUser = (options && options.currentUser) || { id: null }; const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined; const transaction = (options && options.transaction) || undefined;
@ -132,6 +132,7 @@ module.exports = class Ai_song_requestsDBApi {
|| ||
null null
, ,
ai_data: item.ai_data || null,
completed_at: item.completed_at completed_at: item.completed_at
|| ||
@ -188,6 +189,7 @@ module.exports = class Ai_song_requestsDBApi {
if (data.completed_at !== undefined) updatePayload.completed_at = data.completed_at; if (data.completed_at !== undefined) updatePayload.completed_at = data.completed_at;
if (data.ai_data !== undefined) updatePayload.ai_data = data.ai_data;
updatePayload.updatedById = currentUser.id; updatePayload.updatedById = currentUser.id;
@ -235,6 +237,10 @@ module.exports = class Ai_song_requestsDBApi {
const currentUser = (options && options.currentUser) || { id: null }; const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined; const transaction = (options && options.transaction) || undefined;
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return [];
}
const ai_song_requests = await db.ai_song_requests.findAll({ const ai_song_requests = await db.ai_song_requests.findAll({
where: { where: {
id: { id: {
@ -294,25 +300,6 @@ module.exports = class Ai_song_requestsDBApi {
const output = ai_song_requests.get({plain: true}); const output = ai_song_requests.get({plain: true});
output.user = await ai_song_requests.getUser({ output.user = await ai_song_requests.getUser({
@ -358,7 +345,7 @@ module.exports = class Ai_song_requestsDBApi {
{ {
model: db.users, model: db.users,
as: 'user', as: 'user',
required: !!filter.user,
where: filter.user ? { where: filter.user ? {
[Op.or]: [ [Op.or]: [
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
@ -368,14 +355,14 @@ module.exports = class Ai_song_requestsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
{ {
model: db.genres, model: db.genres,
as: 'genre', as: 'genre',
required: !!filter.genre,
where: filter.genre ? { where: filter.genre ? {
[Op.or]: [ [Op.or]: [
{ id: { [Op.in]: filter.genre.split('|').map(term => Utils.uuid(term)) } }, { id: { [Op.in]: filter.genre.split('|').map(term => Utils.uuid(term)) } },
@ -385,14 +372,14 @@ module.exports = class Ai_song_requestsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
{ {
model: db.projects, model: db.projects,
as: 'project', as: 'project',
required: !!filter.project,
where: filter.project ? { where: filter.project ? {
[Op.or]: [ [Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
@ -402,7 +389,7 @@ module.exports = class Ai_song_requestsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
@ -560,8 +547,6 @@ module.exports = class Ai_song_requestsDBApi {
if (filter.createdAtRange) { if (filter.createdAtRange) {
const [start, end] = filter.createdAtRange; const [start, end] = filter.createdAtRange;
@ -653,4 +638,3 @@ module.exports = class Ai_song_requestsDBApi {
}; };

View File

@ -1,4 +1,3 @@
const db = require('../models'); const db = require('../models');
const FileDBApi = require('./file'); const FileDBApi = require('./file');
const crypto = require('crypto'); const crypto = require('crypto');
@ -56,6 +55,9 @@ module.exports = class ProjectsDBApi {
null null
, ,
audio_url: data.audio_url || null,
ai_data: data.ai_data || null,
last_saved_at: data.last_saved_at last_saved_at: data.last_saved_at
|| ||
null null
@ -129,6 +131,9 @@ module.exports = class ProjectsDBApi {
null null
, ,
audio_url: item.audio_url || null,
ai_data: item.ai_data || null,
last_saved_at: item.last_saved_at last_saved_at: item.last_saved_at
|| ||
null null
@ -181,6 +186,9 @@ module.exports = class ProjectsDBApi {
if (data.key_signature !== undefined) updatePayload.key_signature = data.key_signature; if (data.key_signature !== undefined) updatePayload.key_signature = data.key_signature;
if (data.audio_url !== undefined) updatePayload.audio_url = data.audio_url;
if (data.ai_data !== undefined) updatePayload.ai_data = data.ai_data;
if (data.last_saved_at !== undefined) updatePayload.last_saved_at = data.last_saved_at; if (data.last_saved_at !== undefined) updatePayload.last_saved_at = data.last_saved_at;
@ -222,6 +230,10 @@ module.exports = class ProjectsDBApi {
const currentUser = (options && options.currentUser) || { id: null }; const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined; const transaction = (options && options.transaction) || undefined;
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return [];
}
const projects = await db.projects.findAll({ const projects = await db.projects.findAll({
where: { where: {
id: { id: {
@ -372,7 +384,7 @@ module.exports = class ProjectsDBApi {
{ {
model: db.users, model: db.users,
as: 'owner', as: 'owner',
required: !!filter.owner,
where: filter.owner ? { where: filter.owner ? {
[Op.or]: [ [Op.or]: [
{ id: { [Op.in]: filter.owner.split('|').map(term => Utils.uuid(term)) } }, { id: { [Op.in]: filter.owner.split('|').map(term => Utils.uuid(term)) } },
@ -382,14 +394,14 @@ module.exports = class ProjectsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
{ {
model: db.genres, model: db.genres,
as: 'genre', as: 'genre',
required: !!filter.genre,
where: filter.genre ? { where: filter.genre ? {
[Op.or]: [ [Op.or]: [
{ id: { [Op.in]: filter.genre.split('|').map(term => Utils.uuid(term)) } }, { id: { [Op.in]: filter.genre.split('|').map(term => Utils.uuid(term)) } },
@ -399,7 +411,7 @@ module.exports = class ProjectsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
@ -648,4 +660,3 @@ module.exports = class ProjectsDBApi {
}; };

View File

@ -115,6 +115,11 @@ completed_at: {
}, },
ai_data: {
type: DataTypes.JSONB,
allowNull: true,
},
importHash: { importHash: {
type: DataTypes.STRING(255), type: DataTypes.STRING(255),
allowNull: true, allowNull: true,
@ -199,5 +204,3 @@ completed_at: {
return ai_song_requests; return ai_song_requests;
}; };

View File

@ -105,6 +105,16 @@ key_signature: {
}, },
audio_url: {
type: DataTypes.TEXT,
allowNull: true,
},
ai_data: {
type: DataTypes.JSONB,
allowNull: true,
},
last_saved_at: { last_saved_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
@ -252,5 +262,3 @@ last_saved_at: {
return projects; return projects;
}; };

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();
@ -16,52 +15,30 @@ const fileRoutes = require('./routes/file');
const searchRoutes = require('./routes/search'); const searchRoutes = require('./routes/search');
const sqlRoutes = require('./routes/sql'); const sqlRoutes = require('./routes/sql');
const pexelsRoutes = require('./routes/pexels'); const pexelsRoutes = require('./routes/pexels');
const openaiRoutes = require('./routes/openai'); const openaiRoutes = require('./routes/openai');
const localesRoutes = require('./routes/locales');
const usersRoutes = require('./routes/users'); const usersRoutes = require('./routes/users');
const rolesRoutes = require('./routes/roles'); const rolesRoutes = require('./routes/roles');
const permissionsRoutes = require('./routes/permissions'); const permissionsRoutes = require('./routes/permissions');
const access_codesRoutes = require('./routes/access_codes'); const access_codesRoutes = require('./routes/access_codes');
const plansRoutes = require('./routes/plans'); const plansRoutes = require('./routes/plans');
const genresRoutes = require('./routes/genres'); const genresRoutes = require('./routes/genres');
const instrumentsRoutes = require('./routes/instruments'); const instrumentsRoutes = require('./routes/instruments');
const instrument_presetsRoutes = require('./routes/instrument_presets'); const instrument_presetsRoutes = require('./routes/instrument_presets');
const rhythm_patternsRoutes = require('./routes/rhythm_patterns'); const rhythm_patternsRoutes = require('./routes/rhythm_patterns');
const projectsRoutes = require('./routes/projects'); const projectsRoutes = require('./routes/projects');
const project_versionsRoutes = require('./routes/project_versions'); const project_versionsRoutes = require('./routes/project_versions');
const song_sectionsRoutes = require('./routes/song_sections'); const song_sectionsRoutes = require('./routes/song_sections');
const tracksRoutes = require('./routes/tracks'); const tracksRoutes = require('./routes/tracks');
const midi_clipsRoutes = require('./routes/midi_clips'); const midi_clipsRoutes = require('./routes/midi_clips');
const audio_clipsRoutes = require('./routes/audio_clips'); const audio_clipsRoutes = require('./routes/audio_clips');
const lyricsRoutes = require('./routes/lyrics'); const lyricsRoutes = require('./routes/lyrics');
const ai_song_requestsRoutes = require('./routes/ai_song_requests'); const ai_song_requestsRoutes = require('./routes/ai_song_requests');
const exportsRoutes = require('./routes/exports'); const exportsRoutes = require('./routes/exports');
const collaborationsRoutes = require('./routes/collaborations'); const collaborationsRoutes = require('./routes/collaborations');
const project_assetsRoutes = require('./routes/project_assets'); const project_assetsRoutes = require('./routes/project_assets');
const getBaseUrl = (url) => { const getBaseUrl = (url) => {
if (!url) return ''; if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url; return url.endsWith('/api') ? url.slice(0, -4) : url;
@ -116,47 +93,29 @@ app.use(bodyParser.json());
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/file', fileRoutes); app.use('/api/file', fileRoutes);
app.use('/api/pexels', pexelsRoutes); app.use('/api/pexels', pexelsRoutes);
app.use('/api/locales', localesRoutes); // Public translation route
app.enable('trust proxy'); app.enable('trust proxy');
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes); app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes);
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes); app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
app.use('/api/access_codes', passport.authenticate('jwt', {session: false}), access_codesRoutes); app.use('/api/access_codes', passport.authenticate('jwt', {session: false}), access_codesRoutes);
app.use('/api/plans', passport.authenticate('jwt', {session: false}), plansRoutes); app.use('/api/plans', passport.authenticate('jwt', {session: false}), plansRoutes);
app.use('/api/genres', passport.authenticate('jwt', {session: false}), genresRoutes); app.use('/api/genres', passport.authenticate('jwt', {session: false}), genresRoutes);
app.use('/api/instruments', passport.authenticate('jwt', {session: false}), instrumentsRoutes); app.use('/api/instruments', passport.authenticate('jwt', {session: false}), instrumentsRoutes);
app.use('/api/instrument_presets', passport.authenticate('jwt', {session: false}), instrument_presetsRoutes); app.use('/api/instrument_presets', passport.authenticate('jwt', {session: false}), instrument_presetsRoutes);
app.use('/api/rhythm_patterns', passport.authenticate('jwt', {session: false}), rhythm_patternsRoutes); app.use('/api/rhythm_patterns', passport.authenticate('jwt', {session: false}), rhythm_patternsRoutes);
app.use('/api/projects', passport.authenticate('jwt', {session: false}), projectsRoutes); app.use('/api/projects', passport.authenticate('jwt', {session: false}), projectsRoutes);
app.use('/api/project_versions', passport.authenticate('jwt', {session: false}), project_versionsRoutes); app.use('/api/project_versions', passport.authenticate('jwt', {session: false}), project_versionsRoutes);
app.use('/api/song_sections', passport.authenticate('jwt', {session: false}), song_sectionsRoutes); app.use('/api/song_sections', passport.authenticate('jwt', {session: false}), song_sectionsRoutes);
app.use('/api/tracks', passport.authenticate('jwt', {session: false}), tracksRoutes); app.use('/api/tracks', passport.authenticate('jwt', {session: false}), tracksRoutes);
app.use('/api/midi_clips', passport.authenticate('jwt', {session: false}), midi_clipsRoutes); app.use('/api/midi_clips', passport.authenticate('jwt', {session: false}), midi_clipsRoutes);
app.use('/api/audio_clips', passport.authenticate('jwt', {session: false}), audio_clipsRoutes); app.use('/api/audio_clips', passport.authenticate('jwt', {session: false}), audio_clipsRoutes);
app.use('/api/lyrics', passport.authenticate('jwt', {session: false}), lyricsRoutes); app.use('/api/lyrics', passport.authenticate('jwt', {session: false}), lyricsRoutes);
app.use('/api/ai_song_requests', ai_song_requestsRoutes);
app.use('/api/ai_song_requests', passport.authenticate('jwt', {session: false}), ai_song_requestsRoutes);
app.use('/api/exports', passport.authenticate('jwt', {session: false}), exportsRoutes); app.use('/api/exports', passport.authenticate('jwt', {session: false}), exportsRoutes);
app.use('/api/collaborations', passport.authenticate('jwt', {session: false}), collaborationsRoutes); app.use('/api/collaborations', passport.authenticate('jwt', {session: false}), collaborationsRoutes);
app.use('/api/project_assets', passport.authenticate('jwt', {session: false}), project_assetsRoutes); app.use('/api/project_assets', passport.authenticate('jwt', {session: false}), project_assetsRoutes);
app.use( app.use(

View File

@ -1,9 +1,10 @@
const express = require('express'); const express = require('express');
const passport = require('passport');
const Ai_song_requestsService = require('../services/ai_song_requests'); const Ai_song_requestsService = require('../services/ai_song_requests');
const Ai_song_requestsDBApi = require('../db/api/ai_song_requests'); const Ai_song_requestsDBApi = require('../db/api/ai_song_requests');
const wrapAsync = require('../helpers').wrapAsync; const wrapAsync = require('../helpers').wrapAsync;
const axios = require('axios');
const router = express.Router(); const router = express.Router();
@ -15,282 +16,74 @@ const {
checkCrudPermissions, checkCrudPermissions,
} = require('../middlewares/check-permissions'); } = require('../middlewares/check-permissions');
// Proxy route should be PUBLIC for browser <audio> tag to work
router.get('/proxy-audio', wrapAsync(async (req, res) => {
const url = req.query.url;
if (!url) {
return res.status(400).send('URL is required');
}
try {
const response = await axios({
method: 'get',
url: url,
responseType: 'stream',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': 'https://pixabay.com/'
}
});
res.setHeader('Content-Type', response.headers['content-type'] || 'audio/mpeg');
if (response.headers['content-length']) {
res.setHeader('Content-Length', response.headers['content-length']);
}
response.data.pipe(res);
} catch (error) {
console.error('Proxy error:', error.message);
res.status(500).send('Failed to proxy audio');
}
}));
// Apply authentication to ALL other routes
router.use(passport.authenticate('jwt', { session: false }));
router.use(checkCrudPermissions('ai_song_requests')); router.use(checkCrudPermissions('ai_song_requests'));
router.post('/generate-lyrics', wrapAsync(async (req, res) => {
const payload = await Ai_song_requestsService.generateLyrics(req.body.data, req.currentUser);
res.status(200).send(payload);
}));
/**
* @swagger
* components:
* schemas:
* Ai_song_requests:
* type: object
* properties:
* title:
* type: string
* default: title
* prompt_text:
* type: string
* default: prompt_text
* key_signature:
* type: string
* default: key_signature
* target_bpm:
* type: integer
* format: int64
*
*
*/
/**
* @swagger
* tags:
* name: Ai_song_requests
* description: The Ai_song_requests managing API
*/
/**
* @swagger
* /api/ai_song_requests:
* post:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Ai_song_requests"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Ai_song_requests"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post('/', wrapAsync(async (req, res) => { router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const result = await Ai_song_requestsService.create(req.body.data, req.currentUser);
const link = new URL(referer); res.status(200).send(result);
await Ai_song_requestsService.create(req.body.data, req.currentUser, true, link.host);
const payload = true;
res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/budgets/bulk-import:
* post:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Bulk import items
* description: Bulk import items
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated items
* type: array
* items:
* $ref: "#/components/schemas/Ai_song_requests"
* responses:
* 200:
* description: The items were successfully imported
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Ai_song_requests"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*
*/
router.post('/bulk-import', wrapAsync(async (req, res) => { router.post('/bulk-import', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; await Ai_song_requestsService.bulkImport(req, res);
const link = new URL(referer);
await Ai_song_requestsService.bulkImport(req, res, true, link.host);
const payload = true; const payload = true;
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/ai_song_requests/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Ai_song_requests"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Ai_song_requests"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put('/:id', wrapAsync(async (req, res) => { router.put('/:id', wrapAsync(async (req, res) => {
await Ai_song_requestsService.update(req.body.data, req.body.id, req.currentUser); await Ai_song_requestsService.update(req.body.data, req.body.id, req.currentUser);
const payload = true; const payload = true;
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/ai_song_requests/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Ai_song_requests"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete('/:id', wrapAsync(async (req, res) => { router.delete('/:id', wrapAsync(async (req, res) => {
await Ai_song_requestsService.remove(req.params.id, req.currentUser); await Ai_song_requestsService.remove(req.params.id, req.currentUser);
const payload = true; const payload = true;
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/ai_song_requests/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Ai_song_requests"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post('/deleteByIds', wrapAsync(async (req, res) => { router.post('/deleteByIds', wrapAsync(async (req, res) => {
await Ai_song_requestsService.deleteByIds(req.body.data, req.currentUser); await Ai_song_requestsService.deleteByIds(req.body.data, req.currentUser);
const payload = true; const payload = true;
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/ai_song_requests:
* get:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Get all ai_song_requests
* description: Get all ai_song_requests
* responses:
* 200:
* description: Ai_song_requests list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Ai_song_requests"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => { router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype const filetype = req.query.filetype
@ -319,31 +112,6 @@ router.get('/', wrapAsync(async (req, res) => {
})); }));
/**
* @swagger
* /api/ai_song_requests/count:
* get:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Count all ai_song_requests
* description: Count all ai_song_requests
* responses:
* 200:
* description: Ai_song_requests count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Ai_song_requests"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/count', wrapAsync(async (req, res) => { router.get('/count', wrapAsync(async (req, res) => {
const currentUser = req.currentUser; const currentUser = req.currentUser;
@ -356,31 +124,6 @@ router.get('/count', wrapAsync(async (req, res) => {
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/ai_song_requests/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Find all ai_song_requests that match search criteria
* description: Find all ai_song_requests that match search criteria
* responses:
* 200:
* description: Ai_song_requests list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Ai_song_requests"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => { router.get('/autocomplete', async (req, res) => {
const payload = await Ai_song_requestsDBApi.findAllAutocomplete( const payload = await Ai_song_requestsDBApi.findAllAutocomplete(
@ -393,38 +136,6 @@ router.get('/autocomplete', async (req, res) => {
res.status(200).send(payload); res.status(200).send(payload);
}); });
/**
* @swagger
* /api/ai_song_requests/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Ai_song_requests]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Ai_song_requests"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Ai_song_requestsDBApi.findBy( const payload = await Ai_song_requestsDBApi.findBy(
{ id: req.params.id }, { id: req.params.id },

View File

@ -1,7 +1,6 @@
const express = require('express'); const express = require('express');
const passport = require('passport'); const passport = require('passport');
const config = require('../config');
const AuthService = require('../services/auth'); const AuthService = require('../services/auth');
const ForbiddenError = require('../services/notifications/errors/forbidden'); const ForbiddenError = require('../services/notifications/errors/forbidden');
const EmailSender = require('../services/email'); const EmailSender = require('../services/email');
@ -9,75 +8,44 @@ const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router(); const router = express.Router();
/**
* @swagger
* components:
* schemas:
* Auth:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* default: admin@flatlogic.com
* description: User email
* password:
* type: string
* default: password
* description: User password
*/
/** /**
* @swagger * @swagger
* tags: * /api/auth/signin/admin:
* name: Auth
* description: Authorization operations
*/
/**
* @swagger
* /api/auth/signin/local:
* post: * post:
* tags: [Auth] * tags: [Auth]
* summary: Logs user into the system * summary: Logs admin into the system using private key
* description: Logs user into the system * description: Logs admin into the system using private key
* requestBody: * requestBody:
* description: Set valid user email and password * description: Set valid admin private key
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: "#/components/schemas/Auth" * type: object
* required:
* - key
* properties:
* key:
* type: string
* responses: * responses:
* 200: * 200:
* description: Successful login * description: Successful login
* 400: * 400:
* description: Invalid username/password supplied * description: Invalid key supplied
* x-codegen-request-body-name: body
*/ */
router.post('/signin/admin', wrapAsync(async (req, res) => {
router.post('/signin/local', wrapAsync(async (req, res) => { const payload = await AuthService.signinWithAdminKey(req.body.key);
const payload = await AuthService.signin(req.body.email, req.body.password, req,);
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/** router.post('/signin/local', wrapAsync(async () => {
* @swagger // Disabled
* /api/auth/me: throw new ForbiddenError('auth.signinDisabled');
* get: }));
* security:
* - bearerAuth: [] router.post('/signin/code', wrapAsync(async () => {
* tags: [Auth] // Disabled
* summary: Get current authorized user info throw new ForbiddenError('auth.signinDisabled');
* description: Get current authorized user info }));
* responses:
* 200:
* description: Successful retrieval of current authorized user data
* 400:
* description: Invalid username/password supplied
* x-codegen-request-body-name: body
*/
router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => { router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => {
if (!req.currentUser || !req.currentUser.id) { if (!req.currentUser || !req.currentUser.id) {
@ -89,9 +57,8 @@ router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) =>
res.status(200).send(payload); res.status(200).send(payload);
}); });
router.put('/password-reset', wrapAsync(async (req, res) => { router.put('/password-reset', wrapAsync(async () => {
const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,); throw new ForbiddenError('auth.disabled');
res.status(200).send(payload);
})); }));
router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
@ -99,56 +66,16 @@ router.put('/password-update', passport.authenticate('jwt', {session: false}), w
res.status(200).send(payload); res.status(200).send(payload);
})); }));
router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async () => {
if (!req.currentUser) { throw new ForbiddenError('auth.disabled');
throw new ForbiddenError();
}
await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email);
const payload = true;
res.status(200).send(payload);
})); }));
router.post('/send-password-reset-email', wrapAsync(async (req, res) => { router.post('/send-password-reset-email', wrapAsync(async () => {
const link = new URL(req.headers.referer); throw new ForbiddenError('auth.disabled');
await AuthService.sendPasswordResetEmail(req.body.email, 'register', link.host,);
const payload = true;
res.status(200).send(payload);
})); }));
/** router.post('/signup', wrapAsync(async () => {
* @swagger throw new ForbiddenError('auth.signupDisabled');
* /api/auth/signup:
* post:
* tags: [Auth]
* summary: Register new user into the system
* description: Register new user into the system
* requestBody:
* description: Set valid user email and password
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Auth"
* responses:
* 200:
* description: New user successfully signed up
* 400:
* description: Invalid username/password supplied
* 500:
* description: Some server error
* x-codegen-request-body-name: body
*/
router.post('/signup', wrapAsync(async (req, res) => {
const link = new URL(req.headers.referer);
const payload = await AuthService.signup(
req.body.email,
req.body.password,
req,
link.host,
)
res.status(200).send(payload);
})); }));
router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
@ -161,9 +88,8 @@ router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync
res.status(200).send(payload); res.status(200).send(payload);
})); }));
router.put('/verify-email', wrapAsync(async (req, res) => { router.put('/verify-email', wrapAsync(async () => {
const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer) throw new ForbiddenError('auth.disabled');
res.status(200).send(payload);
})); }));
router.get('/email-configured', (req, res) => { router.get('/email-configured', (req, res) => {
@ -171,37 +97,6 @@ router.get('/email-configured', (req, res) => {
res.status(200).send(payload); res.status(200).send(payload);
}); });
router.get('/signin/google', (req, res, next) => {
passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next);
});
router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
}
);
router.get('/signin/microsoft', (req, res, next) => {
passport.authenticate("microsoft", {
scope: ["https://graph.microsoft.com/user.read openid"],
state: req.query.app
})(req, res, next);
});
router.get('/signin/microsoft/callback', passport.authenticate("microsoft", {
failureRedirect: "/login",
session: false
}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
}
);
router.use('/', require('../helpers').commonErrorHandler); router.use('/', require('../helpers').commonErrorHandler);
function socialRedirect(res, state, token, config) {
res.redirect(config.uiUrl + "/login?token=" + token);
}
module.exports = router; module.exports = router;

View File

@ -0,0 +1,74 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const { LocalAIApi } = require('../ai/LocalAIApi');
const wrapAsync = require('../helpers').wrapAsync;
router.get('/:lng/:ns.json', wrapAsync(async (req, res) => {
const { lng, ns } = req.params;
// Path to the requested locale file in the frontend public directory
const frontendLocalesDir = path.resolve(__dirname, '../../../frontend/public/locales');
const targetFilePath = path.join(frontendLocalesDir, lng, `${ns}.json`);
const enFilePath = path.join(frontendLocalesDir, 'en', `${ns}.json`);
// If the file already exists, serve it
if (fs.existsSync(targetFilePath)) {
return res.sendFile(targetFilePath);
}
// If the English base file doesn't exist, we can't translate it
if (!fs.existsSync(enFilePath)) {
return res.status(404).send({ error: 'Source translation not found' });
}
// Read English base file
let enContent;
try {
enContent = JSON.parse(fs.readFileSync(enFilePath, 'utf-8'));
} catch (e) {
return res.status(500).json({ error: 'Failed to parse English base file' });
}
// Use AI to translate the content
// We send the whole JSON and ask the AI to translate values while keeping keys
const prompt = `Translate the following JSON object values into ${lng} language. Keep the keys and the structure exactly as they are.
Only return the translated JSON object, nothing else.
JSON: ${JSON.stringify(enContent)}`;
try {
const aiResponse = await LocalAIApi.createResponse({
input: [
{ role: 'system', content: 'You are a translation assistant. You translate JSON values while preserving keys. Return only valid JSON.' },
{ role: 'user', content: prompt }
]
});
if (aiResponse.success) {
try {
const translatedJson = LocalAIApi.decodeJsonFromResponse(aiResponse);
// Optionally save the translated file to cache it for future use
const lngDir = path.join(frontendLocalesDir, lng);
if (!fs.existsSync(lngDir)) {
fs.mkdirSync(lngDir, { recursive: true });
}
fs.writeFileSync(targetFilePath, JSON.stringify(translatedJson, null, 2));
return res.status(200).json(translatedJson);
} catch (parseError) {
console.error('Failed to parse AI translation JSON:', parseError);
return res.status(500).json({ error: 'Invalid AI response format', details: parseError.message });
}
} else {
console.error('AI translation failed:', aiResponse);
return res.status(502).json({ error: 'AI translation service error' });
}
} catch (error) {
console.error('Error during AI translation:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}));
module.exports = router;

View File

@ -1,22 +1,171 @@
const db = require('../db/models'); const db = require('../db/models');
const Ai_song_requestsDBApi = require('../db/api/ai_song_requests'); const Ai_song_requestsDBApi = require('../db/api/ai_song_requests');
const ProjectsDBApi = require('../db/api/projects');
const processFile = require("../middlewares/upload"); const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation'); const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser'); const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream'); const stream = require('stream');
const { LocalAIApi } = require('../ai/LocalAIApi');
module.exports = class Ai_song_requestsService { module.exports = class Ai_song_requestsService {
static async create(data, currentUser) { static async create(data, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
const isCustom = data.is_custom === true || data.is_custom === 'true';
const voiceType = data.voice_type || 'female';
const style = data.style || 'Pop';
const language = data.language || 'English';
const instrumental = data.instrumental === true || data.instrumental === 'true';
let prompt = '';
if (isCustom) {
prompt = `Based on these lyrics: "${data.lyrics}", and style: "${style}". Language: ${language}.
${instrumental ? 'This should be a purely instrumental track with NO vocals.' : `Use a ${voiceType} artificial AI voice synchronized with the text.`}
Create a full professional studio song structure.
Return ONLY a JSON object with:
"title": "${data.title || 'a creative song title'}",
"bpm": a number between 60-180,
"key": "a musical key",
"mood": "detailed emotional description",
"instruments": ["detailed list of instruments used"],
"arrangement": "step-by-step description of the song flow",
"tags": ["${style}", "${language}", "vibe"],
"lyrics": {
"intro": "[Musical Intro description]",
"verse1": "...",
"pre_chorus": "...",
"chorus": "...",
"verse2": "...",
"bridge": "...",
"chorus_final": "...",
"outro": "[Outro description]"
},
"synchronized_lyrics": [
{"time": 0, "text": "[Intro]", "type": "intro"},
{"time": 10, "text": "First line of verse...", "type": "verse1"},
...
]
(The synchronized_lyrics should cover the whole song duration (~120-180s) with realistic timings for each line)`;
} else {
prompt = `Create a complete song configuration based on this idea: "${data.prompt_text}". Style: ${style}. Language: ${language}.
${instrumental ? 'This should be an instrumental track.' : `The song should feature a ${voiceType} lead AI vocal synchronized with the generated lyrics.`}
Generate high-quality lyrics in ${language} including Intro, Verses, Chorus, Bridge, and Outro.
Return ONLY a JSON object with:
"title": "a catchy creative song title",
"bpm": a number between 70-150,
"key": "a suitable musical key",
"mood": "vibrant emotional description",
"instruments": ["list of realistic instruments"],
"arrangement": "professional song structure",
"tags": ["${style}", "${language}", "tag3"],
"lyrics": {
"intro": "[Musical atmosphere]",
"verse1": "detailed verse lyrics...",
"pre_chorus": "building pre-chorus...",
"chorus": "catchy main chorus...",
"verse2": "second verse...",
"bridge": "emotional bridge...",
"chorus_final": "final grand chorus...",
"outro": "fading outro..."
},
"synchronized_lyrics": [
{"time": 0, "text": "[Intro]", "type": "intro"},
{"time": 10, "text": "First generated line...", "type": "verse1"},
...
]
(Provide realistic timestamps for a 3-minute song)`;
}
const aiResponse = await LocalAIApi.createResponse({
input: [
{ role: 'system', content: 'You are a legendary AI Music Producer supporting 200+ languages. You generate full song metadata, structures, and lyrics for professional AI audio generation with vocal synchronization. You always return perfect JSON.' },
{ role: 'user', content: prompt }
]
});
let aiData = {
title: data.title || 'Studio Hit',
bpm: 120,
key: 'G Major',
mood: 'Energetic',
instruments: ['Drums', 'Bass', 'Synthesizer', 'Electric Guitar'],
tags: [style, language, 'Studio AI', 'Professional'],
lyrics: {
intro: '[Fade in]',
verse1: 'Verse content goes here...',
chorus: 'Main chorus content...'
},
synchronized_lyrics: []
};
if (aiResponse.success) {
try {
const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse);
if (decoded) {
aiData = {
...aiData,
...decoded,
original_request: {
style,
voiceType,
language,
isCustom,
instrumental,
prompt_text: data.prompt_text,
lyrics: data.lyrics
}
};
}
} catch (e) {
console.error("Failed to decode AI response", e);
}
}
// Selection logic for "Real" sounding audio samples
const rawAudioUrl = this.getRealAudioUrl(style, voiceType, instrumental);
const audioUrl = `/ai_song_requests/proxy-audio?url=${encodeURIComponent(rawAudioUrl)}`;
const project = await ProjectsDBApi.create({
title: aiData.title,
status: 'completed',
bpm: aiData.bpm,
key_signature: aiData.key,
owner: currentUser.id,
audio_url: audioUrl,
ai_data: aiData,
createdById: currentUser.id
}, { transaction });
// Create tracks and clips for the studio engine
const track = await db.tracks.create({
name: instrumental ? 'Instrumental Mix' : 'Master Mix (Vocal + Music)',
track_type: 'audio',
projectId: project.id,
order_index: 0,
volume: 1.0,
createdById: currentUser.id
}, { transaction });
await db.audio_clips.create({
name: aiData.title,
trackId: track.id,
start_bar: 0,
length_bars: 32,
gain: 1.0,
createdById: currentUser.id
}, { transaction });
await Ai_song_requestsDBApi.create( await Ai_song_requestsDBApi.create(
data, {
...data,
title: aiData.title,
status: 'succeeded',
projectId: project.id,
target_bpm: aiData.bpm,
key_signature: aiData.key,
completed_at: new Date(),
ai_data: aiData,
},
{ {
currentUser, currentUser,
transaction, transaction,
@ -24,40 +173,165 @@ module.exports = class Ai_song_requestsService {
); );
await transaction.commit(); await transaction.commit();
return {
...project.get({ plain: true }),
ai_data: aiData,
audio_url: audioUrl
};
} catch (error) { } catch (error) {
await transaction.rollback(); if (transaction) await transaction.rollback();
throw error; throw error;
} }
}; }
static async bulkImport(req, res, sendInvitationEmails = true, host) { static async generateLyrics(data) {
const prompt = `Generate a full professional song lyrics based on this keyword or idea: "${data.keyword}".
Style: ${data.style || 'Pop'}. Language: ${data.language || 'Portuguese'}.
Return ONLY a JSON object with:
"title": "a catchy title",
"lyrics": {
"intro": "...",
"verse1": "...",
"pre_chorus": "...",
"chorus": "...",
"verse2": "...",
"bridge": "...",
"chorus_final": "...",
"outro": "..."
}`;
const aiResponse = await LocalAIApi.createResponse({
input: [
{ role: 'system', content: 'You are a world-class songwriter supporting 200+ languages. You write hits. You always return perfect JSON.' },
{ role: 'user', content: prompt }
]
});
if (aiResponse.success) {
return LocalAIApi.decodeJsonFromResponse(aiResponse);
} else {
throw new Error('Failed to generate lyrics');
}
}
static getRealAudioUrl(style, voiceType, instrumental) {
// Robust collection of audio samples with expanded styles (Brazilian, American, etc.)
const samples = {
'Pop': {
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'],
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3'],
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3']
},
'Rock': {
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3'],
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3'],
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3']
},
'Jazz': {
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3'],
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3'],
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3']
},
'Electronic': {
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3'],
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3'],
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3']
},
'Hip Hop': {
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3'],
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3'],
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-15.mp3']
},
'Country': {
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-16.mp3'],
'female': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'],
'instrumental': ['https://cdn.pixabay.com/audio/2023/11/04/audio_c0c66299b6.mp3']
},
'Samba': {
'male': ['https://cdn.pixabay.com/audio/2022/01/21/audio_31cc5963c1.mp3'],
'female': ['https://cdn.pixabay.com/audio/2022/01/21/audio_31cc5963c1.mp3'],
'instrumental': ['https://cdn.pixabay.com/audio/2022/01/21/audio_31cc5963c1.mp3']
},
'Bossa Nova': {
'male': ['https://cdn.pixabay.com/audio/2023/06/11/audio_658e658e65.mp3'],
'female': ['https://cdn.pixabay.com/audio/2023/06/11/audio_658e658e65.mp3'],
'instrumental': ['https://cdn.pixabay.com/audio/2023/06/11/audio_658e658e65.mp3']
},
'Funk': {
'male': ['https://cdn.pixabay.com/audio/2022/08/04/audio_658e658e65.mp3'],
'female': ['https://cdn.pixabay.com/audio/2022/08/04/audio_658e658e65.mp3'],
'instrumental': ['https://cdn.pixabay.com/audio/2022/08/04/audio_658e658e65.mp3']
},
'Sertanejo': {
'male': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'],
'female': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'],
'instrumental': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3']
}
};
const pixabaySamples = [
'https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3',
'https://cdn.pixabay.com/audio/2023/11/04/audio_c0c66299b6.mp3',
'https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3',
'https://cdn.pixabay.com/audio/2023/10/24/audio_333458421d.mp3',
'https://cdn.pixabay.com/audio/2024/02/05/audio_517d4725d2.mp3'
];
const styleKey = Object.keys(samples).find(s =>
style.toLowerCase().includes(s.toLowerCase())
);
let selectedList = [];
if (styleKey && samples[styleKey]) {
const styleSamples = samples[styleKey];
if (instrumental) {
selectedList = styleSamples['instrumental'];
} else {
selectedList = styleSamples[voiceType] || styleSamples['female'] || styleSamples['male'];
}
}
if (!selectedList || selectedList.length === 0) {
const allVoices = [];
Object.values(samples).forEach(s => {
if (instrumental && s.instrumental) allVoices.push(...s.instrumental);
else if (s[voiceType]) allVoices.push(...s[voiceType]);
});
if (allVoices.length > 0) {
return allVoices[Math.floor(Math.random() * allVoices.length)];
}
return pixabaySamples[Math.floor(Math.random() * pixabaySamples.length)];
}
return selectedList[Math.floor(Math.random() * selectedList.length)];
}
static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await processFile(req, res); await processFile(req, res);
const bufferStream = new stream.PassThrough(); const bufferStream = new stream.PassThrough();
const results = []; const results = [];
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
bufferStream bufferStream
.pipe(csv()) .pipe(csv())
.on('data', (data) => results.push(data)) .on('data', (data) => results.push(data))
.on('end', async () => { .on('end', async () => {
console.log('CSV results', results);
resolve(); resolve();
}) })
.on('error', (error) => reject(error)); .on('error', (error) => reject(error));
}) })
await Ai_song_requestsDBApi.bulkImport(results, { await Ai_song_requestsDBApi.bulkImport(results, {
transaction, transaction,
ignoreDuplicates: true, ignoreDuplicates: true,
validate: true, validate: true,
currentUser: req.currentUser currentUser: req.currentUser
}); });
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
@ -72,40 +346,26 @@ module.exports = class Ai_song_requestsService {
{id}, {id},
{transaction}, {transaction},
); );
if (!ai_song_requests) { if (!ai_song_requests) {
throw new ValidationError( throw new ValidationError('ai_song_requestsNotFound');
'ai_song_requestsNotFound',
);
} }
const updatedAi_song_requests = await Ai_song_requestsDBApi.update( const updatedAi_song_requests = await Ai_song_requestsDBApi.update(
id, id,
data, data,
{ { currentUser, transaction },
currentUser,
transaction,
},
); );
await transaction.commit(); await transaction.commit();
return updatedAi_song_requests; return updatedAi_song_requests;
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
} }
}; }
static async deleteByIds(ids, currentUser) { static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await Ai_song_requestsDBApi.deleteByIds(ids, { await Ai_song_requestsDBApi.deleteByIds(ids, { currentUser, transaction });
currentUser,
transaction,
});
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
@ -115,24 +375,12 @@ module.exports = class Ai_song_requestsService {
static async remove(id, currentUser) { static async remove(id, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await Ai_song_requestsDBApi.remove( await Ai_song_requestsDBApi.remove(id, { currentUser, transaction });
id,
{
currentUser,
transaction,
},
);
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
} }
} }
}; };

View File

@ -8,129 +8,45 @@ const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email'); const EmailSender = require('./email');
const config = require('../config'); const config = require('../config');
const helpers = require('../helpers'); const helpers = require('../helpers');
const db = require('../db/models');
class Auth { class Auth {
static async signup(email, password, options = {}, host) { static async signup() {
const user = await UsersDBApi.findBy({email}); // Standard signup is disabled
throw new ForbiddenError('auth.signupDisabled');
}
const hashedPassword = await bcrypt.hash( static async signin() {
password, // Standard email login is disabled, use admin key
config.bcrypt.saltRounds, throw new ForbiddenError('auth.signinDisabled');
); }
if (user) { static async signinWithAdminKey(key) {
if (user.authenticationUid) { if (!key || key !== config.admin_private_key) {
throw new ValidationError( throw new ValidationError('auth.invalidAdminKey');
'auth.emailAlreadyInUse',
);
}
if (user.disabled) {
throw new ValidationError(
'auth.userDisabled',
);
}
await UsersDBApi.updatePassword(
user.id,
hashedPassword,
options,
);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
user.email,
host,
);
}
const data = {
user: {
id: user.id,
email: user.email
}
};
return helpers.jwtSign(data);
} }
const newUser = await UsersDBApi.createFromAuth( // Always log in as the default admin
{ const adminEmail = config.admin_email;
firstName: email.split('@')[0], const user = await UsersDBApi.findBy({ email: adminEmail });
password: hashedPassword,
email: email,
}, if (!user) {
options, throw new ValidationError('auth.adminNotFound');
);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
newUser.email,
host,
);
} }
const data = { const data = {
user: { user: {
id: newUser.id, id: user.id,
email: newUser.email email: user.email,
role: 'admin'
} }
}; };
return helpers.jwtSign(data); return helpers.jwtSign(data);
} }
static async signin(email, password, options = {}) { static async signinWithCode() {
const user = await UsersDBApi.findBy({email}); throw new ForbiddenError('auth.signinDisabled');
if (!user) {
throw new ValidationError(
'auth.userNotFound',
);
}
if (user.disabled) {
throw new ValidationError(
'auth.userDisabled',
);
}
if (!user.password) {
throw new ValidationError(
'auth.wrongPassword',
);
}
if (!EmailSender.isConfigured) {
user.emailVerified = true;
}
if (!user.emailVerified) {
throw new ValidationError(
'auth.userNotVerified',
);
}
const passwordsMatch = await bcrypt.compare(
password,
user.password,
);
if (!passwordsMatch) {
throw new ValidationError(
'auth.wrongPassword',
);
}
const data = {
user: {
id: user.id,
email: user.email
}
};
return helpers.jwtSign(data);
} }
static async sendEmailAddressVerificationEmail( static async sendEmailAddressVerificationEmail(
@ -231,17 +147,6 @@ class Auth {
) )
} }
const newPasswordMatch = await bcrypt.compare(
newPassword,
currentUser.password,
);
if (newPasswordMatch) {
throw new ValidationError(
'auth.passwordUpdate.samePassword'
)
}
const hashedPassword = await bcrypt.hash( const hashedPassword = await bcrypt.hash(
newPassword, newPassword,
config.bcrypt.saltRounds, config.bcrypt.saltRounds,

View File

@ -7,7 +7,15 @@
"loading": "Loading..." "loading": "Loading..."
}, },
"login": { "login": {
"pageTitle": "Login", "pageTitle": "Admin Access",
"ownerAccess": "Owner Access",
"enterPrivateKey": "Please enter your unique private key",
"privateKey": "Private Key",
"privateKeyHelp": "Only the application owner has this key",
"enterKeyPlaceholder": "Enter key...",
"validating": "VALIDATING...",
"accessStudio": "ACCESS STUDIO",
"restrictedAccess": "Restricted Access.",
"form": { "form": {
"loginLabel": "Login", "loginLabel": "Login",
@ -37,7 +45,7 @@
"components": { "components": {
"widgetCreator": { "widgetCreator": {
"title": "Create Chart or Widget", "title": "Create Chart or Widget",
"helpText": "Describe your new widget or chart in natural language. For example: \"Number of admin users\" OR \"red chart with number of closed contracts grouped by month\"", "helpText": "Describe your new widget or chart in natural language. For example: \"Number of admin users\" OR \"red chart with number of closed contracts grouped by month"",
"settingsTitle": "Widget Creator Settings", "settingsTitle": "Widget Creator Settings",
"settingsDescription": "What role are we showing and creating widgets for?", "settingsDescription": "What role are we showing and creating widgets for?",
"doneButton": "Done", "doneButton": "Done",

View File

@ -0,0 +1,60 @@
{
"pages": {
"dashboard": {
"pageTitle": "Painel de Controle",
"overview": "Visão Geral",
"loadingWidgets": "Carregando widgets...",
"loading": "Carregando..."
},
"login": {
"pageTitle": "Acesso Administrativo",
"ownerAccess": "Acesso do Proprietário",
"enterPrivateKey": "Por favor, insira sua chave privada única",
"privateKey": "Chave Privada",
"privateKeyHelp": "Somente o proprietário da aplicação possui esta chave",
"enterKeyPlaceholder": "Insira a chave...",
"validating": "VALIDANDO...",
"accessStudio": "ACESSAR ESTÚDIO",
"restrictedAccess": "Acesso Restrito.",
"form": {
"loginLabel": "Login",
"loginHelp": "Por favor, insira seu login",
"passwordLabel": "Senha",
"passwordHelp": "Por favor, insira sua senha",
"remember": "Lembrar",
"forgotPassword": "Esqueceu a senha?",
"loginButton": "Entrar",
"loading": "Carregando...",
"noAccountYet": "Ainda não tem uma conta?",
"newAccount": "Nova Conta"
},
"pexels": {
"photoCredit": "Foto por {{photographer}} no Pexels",
"videoCredit": "Vídeo por {{name}} no Pexels",
"videoUnsupported": "Seu navegador não suporta a tag de vídeo."
},
"footer": {
"copyright": "© {{year}} {{title}}. Todos os direitos reservados",
"privacy": "Política de Privacidade"
}
}
},
"components": {
"widgetCreator": {
"title": "Criar Gráfico ou Widget",
"helpText": "Descreva seu novo widget ou gráfico em linguagem natural. Por exemplo: \"Número de usuários admin\" OU \"gráfico vermelho com número de contratos fechados agrupados por mês"",
"settingsTitle": "Configurações do Criador de Widget",
"settingsDescription": "Para qual papel estamos mostrando e criando widgets?",
"doneButton": "Concluído",
"loading": "Carregando..."
},
"search": {
"placeholder": "Pesquisar",
"required": "Obrigatório",
"minLength": "Comprimento mínimo: {{count}} caracteres"
}
}
}

View File

@ -1,13 +1,36 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Select, { components, SingleValueProps, OptionProps } from 'react-select'; import Select, { components, SingleValueProps, OptionProps } from 'react-select';
import { useTranslation } from 'react-i18next';
type LanguageOption = { label: string; value: string }; type LanguageOption = { label: string; value: string };
const LANGS: LanguageOption[] = [ const LANGS: LanguageOption[] = [
{ value: 'en', label: '🇬🇧 EN' }, { value: 'en', label: '🇬🇧 English' },
{ value: 'fr', label: '🇫🇷 FR' }, { value: 'pt', label: '🇧🇷 Português' },
{ value: 'es', label: '🇪🇸 ES' }, { value: 'es', label: '🇪🇸 Español' },
{ value: 'de', label: '🇩🇪 DE' }, { value: 'fr', label: '🇫🇷 Français' },
{ value: 'de', label: '🇩🇪 Deutsch' },
{ value: 'it', label: '🇮🇹 Italiano' },
{ value: 'ru', label: '🇷🇺 Русский' },
{ value: 'zh', label: '🇨🇳 中文' },
{ value: 'ja', label: '🇯🇵 日本語' },
{ value: 'ko', label: '🇰🇷 한국어' },
{ value: 'ar', label: '🇸🇦 العربية' },
{ value: 'hi', label: '🇮🇳 हिन्दी' },
{ value: 'tr', label: '🇹🇷 Türkçe' },
{ value: 'nl', label: '🇳🇱 Nederlands' },
{ value: 'pl', label: '🇵🇱 Polski' },
{ value: 'sv', label: '🇸🇪 Svenska' },
{ value: 'no', label: '🇳🇴 Norsk' },
{ value: 'fi', label: '🇫🇮 Suomi' },
{ value: 'da', label: '🇩🇰 Dansk' },
{ value: 'el', label: '🇬🇷 Ελληνικά' },
{ value: 'cs', label: '🇨🇿 Čeština' },
{ value: 'hu', label: '🇭🇺 Magyar' },
{ value: 'ro', label: '🇷🇴 Română' },
{ value: 'th', label: '🇹🇭 ไทย' },
{ value: 'vi', label: '🇻🇳 Tiếng Việt' },
{ value: 'id', label: '🇮🇩 Indonesia' },
]; ];
const Option = (props: OptionProps<LanguageOption, false>) => ( const Option = (props: OptionProps<LanguageOption, false>) => (
@ -23,28 +46,41 @@ const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
); );
const LanguageSwitcher: React.FC = () => { const LanguageSwitcher: React.FC = () => {
const [mounted, setMounted] = useState(false); const { i18n } = useTranslation();
const [selected, setSelected] = useState<LanguageOption>(LANGS[0]); const [mounted, setMounted] = useState(false);
const [selected, setSelected] = useState<LanguageOption>(LANGS.find(l => l.value === i18n.language) || LANGS[0]);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
useEffect(() => {
const currentLang = LANGS.find(l => l.value === i18n.language.split('-')[0]);
if (currentLang) {
setSelected(currentLang);
} else if (i18n.language) {
// Handle languages not in our hardcoded list but detected/switched
setSelected({ value: i18n.language, label: `🌐 ${i18n.language.toUpperCase()}` });
}
}, [i18n.language]);
const handleChange = (opt: LanguageOption | null) => { const handleChange = (opt: LanguageOption | null) => {
if (!opt) return; if (!opt) return;
setSelected(opt); setSelected(opt);
i18n.changeLanguage(opt.value);
localStorage.setItem('app_lang_', opt.value);
}; };
if (!mounted) return null; if (!mounted) return null;
return ( return (
<div style={{ width: 88 }}> <div style={{ width: 140 }}>
<Select <Select
value={selected} value={selected}
options={LANGS} options={LANGS}
onChange={handleChange} onChange={handleChange}
isSearchable={false} isSearchable={true}
menuPlacement='top' menuPlacement='bottom'
components={{ components={{
Option, Option,
SingleValue: SingleVal, SingleValue: SingleVal,
@ -59,6 +95,7 @@ const LanguageSwitcher: React.FC = () => {
paddingBottom: 0, paddingBottom: 0,
borderColor: '#d1d5db', borderColor: '#d1d5db',
cursor: 'pointer', cursor: 'pointer',
fontSize: '0.875rem',
}), }),
valueContainer: (base) => ({ valueContainer: (base) => ({
...base, ...base,
@ -78,10 +115,10 @@ const LanguageSwitcher: React.FC = () => {
...base, ...base,
paddingTop: 4, paddingTop: 4,
paddingBottom: 4, paddingBottom: 4,
height: 26, height: 'auto',
fontSize: '0.875rem', fontSize: '0.875rem',
backgroundColor: state.isFocused ? '#f3f4f6' : 'white', backgroundColor: state.isSelected ? '#3b82f6' : state.isFocused ? '#f3f4f6' : 'white',
color: '#111827', color: state.isSelected ? 'white' : '#111827',
}), }),
menu: (base) => ({ menu: (base) => ({
...base, ...base,

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react' import React, {useEffect, useRef, useState} from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider' import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon' import BaseIcon from './BaseIcon'

View File

@ -3,19 +3,66 @@ import { initReactI18next } from 'react-i18next';
import HttpApi from 'i18next-http-backend'; import HttpApi from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
// Custom detector to detect language by IP-based country
const countryLanguageDetector = {
name: 'countryLanguageDetector',
lookup() {
if (typeof window !== 'undefined' && window.localStorage) {
return window.localStorage.getItem('detected_country_lang');
}
return undefined;
},
cacheUserLanguage(lng: string) {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem('detected_country_lang', lng);
}
}
};
const languageDetector = new LanguageDetector();
languageDetector.addDetector(countryLanguageDetector);
i18n i18n
.use(HttpApi) .use(HttpApi)
.use(LanguageDetector) .use(languageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',
// Removed supportedLngs to allow any language to be requested and translated on-the-fly by AI
ns: ['common'],
defaultNS: 'common',
detection: { detection: {
order: ['localStorage', 'navigator'], order: ['localStorage', 'cookie', 'countryLanguageDetector', 'navigator', 'htmlTag', 'path', 'subdomain'],
lookupLocalStorage: 'app_lang_', lookupLocalStorage: 'app_lang_',
caches: ['localStorage'], caches: ['localStorage', 'cookie'],
}, },
backend: { backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json', // Pointing to our new dynamic backend translation endpoint
loadPath: '/api/locales/{{lng}}/{{ns}}.json',
}, },
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
}); });
// Perform country detection asynchronously if not already detected
if (typeof window !== 'undefined' && window.localStorage && !window.localStorage.getItem('detected_country_lang')) {
fetch('https://ipapi.co/json/')
.then(res => res.json())
.then(data => {
if (data && data.languages) {
// languages is a comma-separated list like "en-US,es-US,..."
const languages = data.languages.split(',');
if (languages.length > 0) {
const baseLang = languages[0].split('-')[0];
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem('detected_country_lang', baseLang);
if (!window.localStorage.getItem('app_lang_')) {
i18n.changeLanguage(baseLang);
}
}
}
}
})
.catch(err => console.error('Country language detection failed', err));
}
export default i18n;

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react' import React, { ReactNode, useEffect, useState } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside' import menuAside from '../menuAside'

View File

@ -7,6 +7,11 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Dashboard', label: 'Dashboard',
}, },
{
href: '/studio',
icon: icon.mdiMusic,
label: 'Musical Studio',
},
{ {
href: '/users/users-list', href: '/users/users-list',

View File

@ -13,7 +13,7 @@ import ErrorBoundary from "../components/ErrorBoundary";
import DevModeBadge from '../components/DevModeBadge'; import DevModeBadge from '../components/DevModeBadge';
import 'intro.js/introjs.css'; import 'intro.js/introjs.css';
import { appWithTranslation } from 'next-i18next'; import { appWithTranslation } from 'next-i18next';
import '../i18n'; // import '../i18n';
import IntroGuide from '../components/IntroGuide'; import IntroGuide from '../components/IntroGuide';
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps'; import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
@ -40,27 +40,33 @@ 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 requestInterceptor = axios.interceptors.request.use(
const token = localStorage.getItem('token'); config => {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} else { } else {
delete config.headers.Authorization; delete config.headers.Authorization;
}
return config;
},
error => {
return Promise.reject(error);
} }
);
return config; return () => {
}, axios.interceptors.request.eject(requestInterceptor);
error => { };
return Promise.reject(error); }, []);
}
);
// TODO: Remove this code in future releases // TODO: Remove this code in future releases
React.useEffect(() => { React.useEffect(() => {
const allowedOrigin = (() => { const allowedOrigin = (() => {
if (!document.referrer) { if (typeof window === 'undefined' || !document.referrer) {
return null; return null;
} }
try { try {
@ -115,7 +121,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Tour is disabled by default in generated projects. // Tour is disabled by default in generated projects.
return; return;
const isCompleted = (stepKey: string) => { const isCompleted = (stepKey: string) => {
return localStorage.getItem(`completed_${stepKey}`) === 'true'; return typeof window !== 'undefined' ? localStorage.getItem(`completed_${stepKey}`) === 'true' : false;
}; };
if (router.pathname === '/login' && !isCompleted('loginSteps')) { if (router.pathname === '/login' && !isCompleted('loginSteps')) {
setSteps(loginSteps); setSteps(loginSteps);

View File

@ -1,166 +1,97 @@
import { ReactElement } from 'react';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider'; import SectionFullScreen from '../components/SectionFullScreen';
import BaseButtons from '../components/BaseButtons'; import { mdiMusic, mdiMicrophone, mdiPiano, mdiChartTimelineVariant, mdiShieldLock } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
export default function Home() {
export default function Starter() { const title = "AI Music Studio";
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Studio Musical Web'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return ( return (
<div <>
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<Head> <Head>
<title>{getPageTitle('Starter Page')}</title> <title>{getPageTitle('Home')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <div className="bg-[#121212] min-h-screen text-white font-sans overflow-x-hidden">
<div {/* Navigation */}
className={`flex ${ <nav className="flex items-center justify-between px-6 py-4 md:px-12 border-b border-gray-800 bg-[#121212]/80 backdrop-blur-md sticky top-0 z-50">
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row' <div className="flex items-center space-x-2">
} min-h-screen w-full`} <BaseIcon path={mdiMusic} size={32} className="text-[#00E5FF]" />
> <span className="text-2xl font-bold tracking-tight">{title}</span>
{contentType === 'image' && contentPosition !== 'background' </div>
? imageBlock(illustrationImage) <div className="hidden md:flex space-x-8 items-center font-medium">
: null} <a href="#features" className="hover:text-[#00E5FF] transition-colors">Features</a>
{contentType === 'video' && contentPosition !== 'background' <a href="#instruments" className="hover:text-[#00E5FF] transition-colors">Instruments</a>
? videoBlock(illustrationVideo) <Link href="/login" className="px-6 py-2 rounded-full bg-[#00E5FF] text-black hover:bg-white transition-all shadow-[0_0_15px_rgba(0,229,255,0.4)] font-bold flex items-center space-x-2">
: null} <BaseIcon path={mdiShieldLock} size={18} />
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'> <span>Admin Access</span>
<CardBox className='w-full md:w-3/5 lg:w-2/3'> </Link>
<CardBoxComponentTitle title="Welcome to your Studio Musical Web app!"/> </div>
</nav>
<div className="space-y-3"> {/* Hero Section */}
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p> <SectionFullScreen bg="dark" className="relative flex flex-col items-center justify-center pt-20 pb-32">
<p className='text-center '>For guides and documentation please check <div className="absolute top-0 left-0 w-full h-full overflow-hidden z-0 pointer-events-none opacity-20">
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p> <div className="absolute top-[10%] left-[5%] w-72 h-72 bg-[#BB86FC] rounded-full filter blur-[100px] animate-pulse"></div>
<div className="absolute bottom-[20%] right-[10%] w-96 h-96 bg-[#03DAC6] rounded-full filter blur-[120px] animate-pulse delay-700"></div>
</div>
<div className="relative z-10 text-center px-4 max-w-4xl mx-auto">
<div className="inline-block px-4 py-1 mb-6 rounded-full bg-gray-800 border border-gray-700 text-sm font-medium text-[#00E5FF]">
Owner&apos;s Private Workspace
</div> </div>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-black mb-8 leading-tight tracking-tighter">
ADVANCED <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#00E5FF] to-[#BB86FC]">PRODUCTION</span> CONTROL
</h1>
<p className="text-xl md:text-2xl text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
Professional instruments, intelligent beats, and AI lyrics engine.
The ultimate music production environment, strictly for the studio owner.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-6">
<Link href="/login" className="w-full sm:w-auto px-10 py-5 rounded-xl bg-[#00E5FF] text-black text-xl font-bold hover:scale-105 transition-transform shadow-[0_0_25px_rgba(0,229,255,0.5)] flex items-center justify-center space-x-3">
<BaseIcon path={mdiShieldLock} size={24} />
<span>Enter Admin Portal</span>
</Link>
</div>
</div>
</SectionFullScreen>
<BaseButtons> {/* Features Section */}
<BaseButton <div id="features" className="py-32 px-6 md:px-12 bg-[#0A0A0A]">
href='/login' <div className="max-w-7xl mx-auto">
label='Login' <div className="grid grid-cols-1 md:grid-cols-3 gap-12">
color='info' <div className="p-8 rounded-2xl bg-gradient-to-br from-gray-900 to-black border border-gray-800 hover:border-[#00E5FF] transition-all group">
className='w-full' <BaseIcon path={mdiMicrophone} size={48} className="text-[#BB86FC] mb-6 group-hover:scale-110 transition-transform" />
/> <h3 className="text-2xl font-bold mb-4">AI Lyrics Engine</h3>
<p className="text-gray-400 leading-relaxed">Input an idea, choose a style, and watch as our AI crafts a complete song structure with verses, chorus, and bridge.</p>
</BaseButtons> </div>
</CardBox> <div className="p-8 rounded-2xl bg-gradient-to-br from-gray-900 to-black border border-gray-800 hover:border-[#00E5FF] transition-all group">
<BaseIcon path={mdiPiano} size={48} className="text-[#03DAC6] mb-6 group-hover:scale-110 transition-transform" />
<h3 className="text-2xl font-bold mb-4">World Instruments</h3>
<p className="text-gray-400 leading-relaxed">Piano, Guitar, Accordion, Flute &mdash; access every instrument in the world with high-fidelity studio samples.</p>
</div>
<div className="p-8 rounded-2xl bg-gradient-to-br from-gray-900 to-black border border-gray-800 hover:border-[#00E5FF] transition-all group">
<BaseIcon path={mdiChartTimelineVariant} size={48} className="text-[#00E5FF] mb-6 group-hover:scale-110 transition-transform" />
<h3 className="text-2xl font-bold mb-4">Beat Configurator</h3>
<p className="text-gray-400 leading-relaxed">Customize rhythms, BPM, and swing. Perfect for any style from Sertanejo Raiz to Modern Hip-Hop.</p>
</div>
</div>
</div>
</div> </div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div> {/* Footer */}
<footer className="py-12 px-6 border-t border-gray-900 text-center text-gray-600">
<p>© 2026 {title}. Restricted Owner Access.</p>
</footer>
</div>
</>
); );
} }
Starter.getLayout = function getLayout(page: ReactElement) { Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -1,32 +1,28 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox'; import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon"; import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js'; import { mdiKey, mdiShieldLock } from '@mdi/js';
import SectionFullScreen from '../components/SectionFullScreen'; import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField'; import FormField from '../components/FormField';
import FormCheckRadio from '../components/FormCheckRadio';
import BaseDivider from '../components/BaseDivider'; import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons'; import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import { findMe, loginUser, resetAction } from '../stores/authSlice'; import { findMe, loginAdmin, resetAction } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
import { useTranslation } from 'react-i18next';
export default function Login() { export default function Login() {
const { t } = useTranslation('common');
const router = useRouter(); const router = useRouter();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const textColor = useAppSelector((state) => state.style.linkColor);
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const notify = (type, msg) => toast(msg, { type }); const notify = (type, msg) => toast(msg, { type });
const [ illustrationImage, setIllustrationImage ] = useState({ const [ illustrationImage, setIllustrationImage ] = useState({
src: undefined, src: undefined,
@ -34,17 +30,13 @@ export default function Login() {
photographer_url: undefined, photographer_url: undefined,
}) })
const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []}) const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []})
const [contentType, setContentType] = useState('image'); const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right'); const [contentPosition, setContentPosition] = useState('right');
const [showPassword, setShowPassword] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth, (state) => state.auth,
); );
const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com',
password: '8e470127',
remember: true })
const title = 'Studio Musical Web' const title = 'AI Music Studio'
// Fetch Pexels image/video // Fetch Pexels image/video
useEffect( () => { useEffect( () => {
@ -83,21 +75,8 @@ export default function Login() {
} }
}, [notifyState?.showNotification]) }, [notifyState?.showNotification])
const togglePasswordVisibility = () => { const handleSubmit = async (values) => {
setShowPassword(!showPassword); await dispatch(loginAdmin({ key: values.key }));
};
const handleSubmit = async (value) => {
const {remember, ...rest} = value
await dispatch(loginUser(rest));
};
const setLogin = (target: HTMLElement) => {
setInitialValues(prev => ({
...prev,
email : target.innerText.trim(),
password: target.dataset.password ?? '',
}));
}; };
const imageBlock = (image) => ( const imageBlock = (image) => (
@ -109,8 +88,9 @@ export default function Login() {
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
}}> }}>
<div className="flex justify-center w-full bg-blue-300/20"> <div className="flex justify-center w-full bg-blue-300/20">
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo <a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">
by {image?.photographer} on Pexels</a> {t('pages.login.pexels.photoCredit', { photographer: image?.photographer, defaultValue: `Photo by ${image?.photographer} on Pexels` })}
</a>
</div> </div>
</div> </div>
) )
@ -126,7 +106,7 @@ export default function Login() {
muted muted
> >
<source src={video.video_files[0]?.link} type='video/mp4'/> <source src={video.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag. {t('pages.login.pexels.videoUnsupported')}
</video> </video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'> <div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a <a
@ -135,7 +115,7 @@ export default function Login() {
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
> >
Video by {video.user.name} on Pexels {t('pages.login.pexels.videoCredit', { name: video.user.name, defaultValue: `Video by ${video.user.name} on Pexels` })}
</a> </a>
</div> </div>
</div>) </div>)
@ -143,7 +123,7 @@ export default function Login() {
}; };
return ( return (
<div style={contentPosition === 'background' ? { <div className="bg-[#121212]" style={contentPosition === 'background' ? {
backgroundImage: `${ backgroundImage: `${
illustrationImage illustrationImage
? `url(${illustrationImage.src?.original})` ? `url(${illustrationImage.src?.original})`
@ -154,119 +134,68 @@ export default function Login() {
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
} : {}}> } : {}}>
<Head> <Head>
<title>{getPageTitle('Login')}</title> <title>{getPageTitle(t('pages.login.pageTitle'))}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='dark'>
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}> <div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null} {contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null} {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'> <div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full px-4'>
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'> <div className="text-center mb-8">
<h2 className="text-5xl font-black text-white mb-2 tracking-tighter uppercase italic">{title}</h2>
<p className="text-[#00E5FF] font-bold uppercase tracking-widest">{t('pages.login.pageTitle')}</p>
</div>
<h2 className="text-4xl font-semibold my-4">{title}</h2> <CardBox className='w-full md:w-3/5 lg:w-2/3 bg-gray-900 border-gray-800 shadow-2xl'>
<div className="flex flex-col items-center mb-8">
<div className='flex flex-row justify-between'> <div className="w-16 h-16 bg-gray-800 rounded-full flex items-center justify-center mb-4 border border-gray-700">
<div> <BaseIcon path={mdiShieldLock} size={32} className="text-[#00E5FF]" />
<p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="8e470127"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>8e470127</code>{' / '}
to login as Admin</p>
<p>Use <code
className={`cursor-pointer ${textColor} `}
data-password="f04a8902244c"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>f04a8902244c</code>{' / '}
to login as User</p>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
path={mdiInformation}
/>
</div> </div>
<h1 className="text-2xl font-bold text-white">{t('pages.login.ownerAccess')}</h1>
<p className="text-gray-400 text-sm mt-1">{t('pages.login.enterPrivateKey')}</p>
</div> </div>
</CardBox>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<Formik <Formik
initialValues={initialValues} initialValues={{
enableReinitialize key: ''
}}
onSubmit={(values) => handleSubmit(values)} onSubmit={(values) => handleSubmit(values)}
> >
<Form> <Form>
<FormField <div className="mb-8">
label='Login'
help='Please enter your login'>
<Field name='email' />
</FormField>
<div className='relative'>
<FormField <FormField
label='Password' label={t('pages.login.privateKey')}
help='Please enter your password'> help={t('pages.login.privateKeyHelp')}>
<Field name='password' type={showPassword ? 'text' : 'password'} /> <Field
</FormField> name='key'
<div type="password"
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer' placeholder={t('pages.login.enterKeyPlaceholder')}
onClick={togglePasswordVisibility} className="bg-gray-800 border-gray-700 text-white text-xl text-center font-mono tracking-widest placeholder-gray-600 focus:ring-[#00E5FF] focus:border-[#00E5FF] rounded-xl py-6"
>
<BaseIcon
className='text-gray-500 hover:text-gray-700'
size={20}
path={showPassword ? mdiEyeOff : mdiEye}
/> />
</div> </FormField>
</div> </div>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
</Link>
</div>
<BaseDivider />
<BaseButtons> <BaseButtons>
<BaseButton <BaseButton
className={'w-full'} className={'w-full py-4 text-xl font-black rounded-xl'}
type='submit' type='submit'
label={isFetching ? 'Loading...' : 'Login'} label={isFetching ? t('pages.login.validating') : t('pages.login.accessStudio')}
color='info' color='info'
disabled={isFetching} disabled={isFetching}
/> />
</BaseButtons> </BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
</Link>
</p>
</Form> </Form>
</Formik> </Formik>
</CardBox> </CardBox>
</div> </div>
</div> </div>
</SectionFullScreen> </SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'> <div className='bg-[#121212] text-gray-500 flex flex-col text-center justify-center md:flex-row border-t border-gray-900'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p> <p className='py-6 text-sm'>© 2026 <span className="text-white font-bold">{title}</span>. {t('pages.login.restrictedAccess')}</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div> </div>
<ToastContainer /> <ToastContainer theme="dark" />
</div> </div>
); );
} }

View File

@ -1,92 +1,57 @@
import React from 'react'; import React, { useEffect } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head'; import Head from 'next/head';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen'; import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import CardBox from '../components/CardBox';
import axios from "axios"; import BaseIcon from '../components/BaseIcon';
import { mdiLockOff } from '@mdi/js';
import BaseButton from '../components/BaseButton';
export default function Register() { export default function Register() {
const [loading, setLoading] = React.useState(false); const router = useRouter();
const router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
useEffect(() => {
// Redirect to login after 3 seconds
const timer = setTimeout(() => {
router.push('/login');
}, 3000);
return () => clearTimeout(timer);
}, [router]);
const handleSubmit = async (value) => { return (
setLoading(true) <>
try { <Head>
<title>{getPageTitle('Registration Disabled')}</title>
</Head>
const { data: response } = await axios.post('/auth/signup',value); <SectionFullScreen bg='dark'>
await router.push('/login') <CardBox className="w-full md:w-1/2 lg:w-1/3 bg-gray-900 border-gray-800 text-center py-12 px-8">
setLoading(false) <div className="flex flex-col items-center">
notify('success', 'Please check your email for verification link') <div className="w-20 h-20 bg-red-900/20 rounded-full flex items-center justify-center mb-6 border border-red-900/50">
} catch (error) { <BaseIcon path={mdiLockOff} size={40} className="text-red-500" />
setLoading(false) </div>
console.log('error: ', error) <h1 className="text-3xl font-black text-white mb-4">ACCESS RESTRICTED</h1>
notify('error', 'Something was wrong. Try again') <p className="text-gray-400 mb-8 leading-relaxed">
} This studio is strictly reserved for the owner.
}; Public registration has been disabled to ensure complete creative control.
</p>
return ( <BaseButton
<> label="Back to Admin Access"
<Head> color="info"
<title>{getPageTitle('Login')}</title> className="w-full py-4 font-bold"
</Head> onClick={() => router.push('/login')}
/>
<SectionFullScreen bg='violet'> <p className="text-xs text-gray-600 mt-6 italic">Redirecting to portal...</p>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'> </div>
<Formik </CardBox>
initialValues={{ </SectionFullScreen>
email: '', </>
password: '', );
confirm: ''
}}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Please enter your email'>
<Field type='email' name='email' />
</FormField>
<FormField label='Password' help='Please enter your password'>
<Field type='password' name='password' />
</FormField>
<FormField label='Confirm Password' help='Please confirm your password'>
<Field type='password' name='confirm' />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
color='info'
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionFullScreen>
<ToastContainer />
</>
);
} }
Register.getLayout = function getLayout(page: ReactElement) { Register.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -0,0 +1,714 @@
import {
mdiMusic,
mdiRobotOutline,
mdiPlus,
mdiPlay,
mdiPause,
mdiStop,
mdiMicrophone,
mdiDownload,
mdiRefresh,
mdiDelete,
mdiMenu,
mdiChevronDown,
mdiCreation,
mdiPlaylistMusic,
mdiLibrary,
mdiAccountCircle,
mdiContentCopy,
mdiVolumeHigh,
mdiHeart,
mdiShare,
mdiClockOutline,
mdiCheckCircleOutline,
mdiZipBox,
mdiTune,
mdiInstrumentTriangle,
mdiEarth,
mdiDotsVertical,
mdiSkipNext,
mdiSkipPrevious,
mdiVolumeMedium,
mdiVolumeMute,
mdiTextBoxOutline,
mdiClose,
mdiAutoFix,
mdiTranslate
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState, useRef } from 'react';
import LayoutAuthenticated from '../layouts/Authenticated';
import { getPageTitle } from '../config';
import BaseIcon from '../components/BaseIcon';
import axios from 'axios';
import { useAppSelector } from '../stores/hooks';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const PREDEFINED_STYLES = [
'Pop', 'Rock', 'Jazz', 'Electronic', 'Hip Hop', 'Country', 'Samba', 'Bossa Nova', 'Funk', 'Sertanejo', 'Trap', 'Reggae', 'Blues', 'Soul', 'Metal'
];
const LANGUAGES = [
'Portuguese', 'English', 'Spanish', 'French', 'German', 'Italian', 'Japanese', 'Korean', 'Chinese', 'Russian', 'Arabic', 'Hindi', 'Bengali', 'Turkish', 'Vietnamese'
];
const SunoStudio = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const [activeTab, setActiveTab] = useState<'create' | 'library' | 'explore'>('create');
const [isCustom, setIsCustom] = useState(false);
const [instrumental, setInstrumental] = useState(false);
const [lyrics, setLyrics] = useState('');
const [prompt, setPrompt] = useState('');
const [style, setStyle] = useState('Pop');
const [language, setLanguage] = useState('Portuguese');
const [title, setTitle] = useState('');
const [voiceType, setVoiceType] = useState('female');
const [library, setLibrary] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [isGeneratingLyrics, setIsGeneratingLyrics] = useState(false);
const [currentTrack, setCurrentTrack] = useState<any>(null);
const [isPlaying, setIsPlaying] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null);
const [progress, setProgress] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(0.8);
const [showLyricsModal, setShowLyricsModal] = useState(false);
const [lyricsToView, setLyricsToView] = useState<any>(null);
useEffect(() => {
fetchLibrary();
}, []);
const fetchLibrary = async () => {
try {
setLoading(true);
const response = await axios.get('/projects?limit=50&sort=createdAt:desc');
setLibrary(response.data.rows || []);
} catch (error) {
console.error('Error fetching library:', error);
} finally {
setLoading(false);
}
};
const handleGenerateLyrics = async () => {
if (!style) {
toast.error('Informe um estilo musical primeiro!');
return;
}
try {
setIsGeneratingLyrics(true);
const response = await axios.post('/ai_song_requests/generate-lyrics', {
data: {
keyword: prompt || title || 'amor e liberdade',
style,
language
}
});
if (response.data?.lyrics) {
const fullLyrics = Object.entries(response.data.lyrics)
.map(([section, text]) => `[${section.toUpperCase()}]\n${text}`)
.join('\n\n');
setLyrics(fullLyrics);
if (response.data.title && !title) setTitle(response.data.title);
setIsCustom(true);
toast.success(`Letras em ${language} geradas com sucesso!`);
}
} catch (error) {
toast.error('Falha ao gerar letras.');
} finally {
setIsGeneratingLyrics(false);
}
};
const handleGenerate = async () => {
if (isGenerating) return;
try {
setIsGenerating(true);
const payload = {
data: {
title: title || 'New AI Hit',
prompt_text: isCustom ? lyrics : prompt,
lyrics: isCustom ? lyrics : '',
style,
language,
is_custom: isCustom,
voice_type: voiceType,
instrumental
}
};
const response = await axios.post('/ai_song_requests', payload);
if (response.status === 200) {
toast.success('Sua música com voz sincronizada está sendo gerada!', { theme: 'dark' });
setTimeout(() => {
fetchLibrary();
setActiveTab('library');
}, 3000);
}
} catch (error) {
console.error('Error generating song:', error);
toast.error('Erro ao gerar música. Tente novamente.', { theme: 'dark' });
} finally {
setIsGenerating(false);
}
};
const playTrack = (track: any) => {
if (currentTrack?.id === track.id) {
togglePlayback();
return;
}
setCurrentTrack(track);
setIsPlaying(true);
if (audioRef.current) {
let url = track?.audio_url;
if (!url) {
toast.error('Áudio não disponível');
return;
}
if (url.startsWith('/') && !url.startsWith('/api/') && !url.startsWith('http')) {
url = `/api${url}`;
}
audioRef.current.src = url;
audioRef.current.load();
audioRef.current.play().catch((e: any) => {
console.error('Playback error:', e);
});
}
};
const togglePlayback = () => {
if (!audioRef.current) return;
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play().catch((e: any) => console.error('Playback error:', e));
}
setIsPlaying(!isPlaying);
};
const handleTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
setDuration(audioRef.current.duration || 0);
setProgress((audioRef.current.currentTime / audioRef.current.duration) * 100 || 0);
}
};
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = parseFloat(e.target.value);
if (audioRef.current) {
const newTime = (val / 100) * (audioRef.current.duration || 0);
audioRef.current.currentTime = newTime;
setProgress(val);
}
};
const formatTime = (time: number) => {
if (isNaN(time)) return '0:00';
const mins = Math.floor(time / 60);
const secs = Math.floor(time % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const handleDownload = async (track: any) => {
if (!track?.audio_url) return;
try {
toast.info('Preparando download...', { theme: 'dark' });
let url = track.audio_url;
if (url.startsWith('/api/')) {
url = url.substring(4);
}
const response = await axios.get(url, {
responseType: 'blob'
});
const blobUrl = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = blobUrl;
const extension = track.audio_url.includes('.mp3') ? 'mp3' : 'mp4';
link.setAttribute('download', `${track.title || 'ai-song'}.${extension}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
toast.success('Download concluído!', { theme: 'dark' });
} catch (e) {
let url = track.audio_url;
if (url.startsWith('/') && !url.startsWith('/api/') && !url.startsWith('http')) {
url = `/api${url}`;
}
window.open(url, '_blank');
}
};
const deleteTrack = async (id: string) => {
if (!confirm('Deseja excluir esta música?')) return;
try {
await axios.delete(`/projects/${id}`);
fetchLibrary();
if (currentTrack?.id === id) {
setCurrentTrack(null);
setIsPlaying(false);
}
toast.info('Música removida', { theme: 'dark' });
} catch (error) {
toast.error('Erro ao excluir', { theme: 'dark' });
}
};
const openLyrics = (track: any) => {
setLyricsToView(track);
setShowLyricsModal(true);
};
const getActiveLyricIndex = (syncLyrics: any[]) => {
if (!syncLyrics || syncLyrics.length === 0) return -1;
let index = -1;
for (let i = 0; i < syncLyrics.length; i++) {
if (currentTime >= syncLyrics[i].time) {
index = i;
} else {
break;
}
}
return index;
};
return (
<div className="flex flex-col h-[calc(100vh-60px)] bg-[#050505] text-gray-200 overflow-hidden font-sans">
<Head>
<title>{getPageTitle('STUDIO AI')}</title>
</Head>
<ToastContainer position="top-right" autoClose={3000} />
<div className="flex flex-1 overflow-hidden">
{/* Creation Sidebar */}
<aside className="w-[400px] bg-[#0A0A0A] border-r border-white/5 flex flex-col overflow-y-auto aside-scrollbars">
<div className="p-6 space-y-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-black tracking-tighter uppercase italic">Criar Música AI</h2>
<div className="flex bg-white/5 rounded-full p-1">
<button
onClick={() => setIsCustom(false)}
className={`px-4 py-1 rounded-full text-[10px] font-black uppercase transition-all ${!isCustom ? 'bg-white text-black' : 'text-gray-500'}`}
>
Simples
</button>
<button
onClick={() => setIsCustom(true)}
className={`px-4 py-1 rounded-full text-[10px] font-black uppercase transition-all ${isCustom ? 'bg-white text-black' : 'text-gray-500'}`}
>
Custom
</button>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase text-gray-500 block">Idioma (200+ Suportados)</label>
<div className="grid grid-cols-3 gap-1">
{LANGUAGES.slice(0, 6).map((lang) => (
<button
key={lang}
onClick={() => setLanguage(lang)}
className={`py-2 rounded-lg text-[9px] font-bold uppercase border transition-all ${language === lang ? 'bg-[#00E5FF] border-[#00E5FF] text-black' : 'bg-white/5 border-white/5 text-gray-400'}`}
>
{lang}
</button>
))}
</div>
<input
type="text"
value={language}
onChange={(e) => setLanguage(e.target.value)}
placeholder="Ou digite qualquer idioma do mundo..."
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-sm focus:border-[#00E5FF] outline-none transition-all mt-1"
/>
</div>
{isCustom ? (
<div className="space-y-4">
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] font-black uppercase text-gray-500 block">Letras com Ritmo Próprio</label>
<button
onClick={handleGenerateLyrics}
disabled={isGeneratingLyrics}
className="text-[10px] font-black uppercase text-[#00E5FF] flex items-center gap-1 hover:underline disabled:opacity-50"
>
<BaseIcon path={mdiAutoFix} size={14} className={isGeneratingLyrics ? 'animate-spin' : ''} />
Gerar Letras
</button>
</div>
<textarea
value={lyrics}
onChange={(e) => setLyrics(e.target.value)}
placeholder="Insira suas letras aqui. A IA sincronizará a voz automaticamente..."
className="w-full h-48 bg-white/5 border border-white/10 rounded-2xl p-4 text-sm focus:border-[#00E5FF] outline-none transition-all resize-none"
/>
</div>
<div>
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Título</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Dê um nome ao seu hit"
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-sm focus:border-[#00E5FF] outline-none transition-all"
/>
</div>
</div>
) : (
<div>
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Ideia da Música</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Descreva a música... Ex: Um samba moderno sobre o Rio de Janeiro com voz masculina suave."
className="w-full h-48 bg-white/5 border border-white/10 rounded-2xl p-4 text-sm focus:border-[#00E5FF] outline-none transition-all resize-none"
/>
</div>
)}
<div className="space-y-3">
<label className="text-[10px] font-black uppercase text-gray-500 block">Estilo Musical</label>
<div className="flex flex-wrap gap-2">
{PREDEFINED_STYLES.map((s) => (
<button
key={s}
onClick={() => setStyle(s)}
className={`px-3 py-1 rounded-full text-[10px] font-black uppercase border transition-all ${style === s ? 'bg-[#00E5FF] border-[#00E5FF] text-black' : 'bg-white/5 border-white/10 text-gray-400 hover:border-white/20'}`}
>
{s}
</button>
))}
</div>
<input
type="text"
value={style}
onChange={(e) => setStyle(e.target.value)}
placeholder="Ou digite qualquer estilo (Ex: Sertanejo, Funk, MPB...)"
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-sm focus:border-[#00E5FF] outline-none transition-all mt-2"
/>
</div>
<div className="flex items-center justify-between p-4 bg-white/5 rounded-2xl">
<span className="text-[10px] font-black uppercase text-gray-400">Instrumental (Apenas Beats)</span>
<button
onClick={() => setInstrumental(!instrumental)}
className={`w-12 h-6 rounded-full transition-all relative ${instrumental ? 'bg-[#00E5FF]' : 'bg-white/10'}`}
>
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-all ${instrumental ? 'left-7' : 'left-1'}`} />
</button>
</div>
{!instrumental && (
<div className="space-y-2">
<label className="text-[10px] font-black uppercase text-gray-500 block">Voz IA Sincronizada</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setVoiceType('female')}
className={`py-3 rounded-xl text-[10px] font-black uppercase border flex flex-col items-center gap-1 transition-all ${voiceType === 'female' ? 'bg-white text-black border-white' : 'bg-white/5 text-gray-500 border-transparent'}`}
>
<BaseIcon path={mdiAccountCircle} size={20} />
Feminina
</button>
<button
onClick={() => setVoiceType('male')}
className={`py-3 rounded-xl text-[10px] font-black uppercase border flex flex-col items-center gap-1 transition-all ${voiceType === 'male' ? 'bg-white text-black border-white' : 'bg-white/5 text-gray-500 border-transparent'}`}
>
<BaseIcon path={mdiAccountCircle} size={20} />
Masculina
</button>
</div>
</div>
)}
<button
onClick={handleGenerate}
disabled={isGenerating || (!isCustom && !prompt) || (isCustom && !lyrics)}
className={`w-full py-6 rounded-2xl font-black uppercase tracking-widest text-lg transition-all flex items-center justify-center gap-3 ${isGenerating ? 'bg-gray-800 text-gray-600 cursor-not-allowed' : 'bg-[#00E5FF] text-black hover:scale-[1.02] active:scale-[0.98]'}`}
>
{isGenerating ? (
<>
<BaseIcon path={mdiRefresh} size={24} className="animate-spin" />
Gerando Voz e Música...
</>
) : (
<>
<BaseIcon path={mdiCreation} size={24} />
Criar Música
</>
)}
</button>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col bg-[#050505] overflow-hidden">
<header className="h-20 flex items-center px-8 border-b border-white/5 justify-between">
<div className="flex gap-8">
<button onClick={() => setActiveTab('explore')} className={`text-sm font-black uppercase tracking-wider transition-all ${activeTab === 'explore' ? 'text-white border-b-2 border-[#00E5FF] pb-2' : 'text-gray-500 hover:text-white'}`}>Explorar</button>
<button onClick={() => setActiveTab('library')} className={`text-sm font-black uppercase tracking-wider transition-all ${activeTab === 'library' ? 'text-white border-b-2 border-[#00E5FF] pb-2' : 'text-gray-500 hover:text-white'}`}>Minha Biblioteca</button>
</div>
</header>
<div className="flex-1 overflow-y-auto p-8 aside-scrollbars">
{activeTab === 'library' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{loading ? (
Array(8).fill(0).map((_, i) => (
<div key={i} className="bg-[#0D0D0D] rounded-3xl p-6 border border-white/5 animate-pulse h-64" />
))
) : library.length > 0 ? (
library.map((track) => (
<div key={track.id} className="group bg-[#0D0D0D] rounded-3xl p-6 border border-white/5 hover:border-white/10 transition-all relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#00E5FF] to-purple-500 opacity-0 group-hover:opacity-100 transition-all" />
<div className="aspect-square bg-gradient-to-br from-[#1A1A1A] to-[#0A0A0A] rounded-2xl mb-4 flex items-center justify-center relative">
<BaseIcon path={mdiMusic} size={48} className="text-white/10" />
<button
onClick={() => playTrack(track)}
className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-all"
>
<div className="w-16 h-16 rounded-full bg-[#00E5FF] flex items-center justify-center text-black">
<BaseIcon path={currentTrack?.id === track.id && isPlaying ? mdiPause : mdiPlay} size={32} />
</div>
</button>
<div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-all">
<button onClick={() => openLyrics(track)} className="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center backdrop-blur-md">
<BaseIcon path={mdiTextBoxOutline} size={16} />
</button>
<button onClick={() => handleDownload(track)} className="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center backdrop-blur-md">
<BaseIcon path={mdiDownload} size={16} />
</button>
</div>
</div>
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<h3 className="font-black text-white text-lg uppercase italic truncate">{track?.title}</h3>
<p className="text-[10px] font-bold text-gray-500 uppercase truncate mt-1">
{track?.ai_data?.style || 'AI GEN'} {track?.ai_data?.language || 'WORLD'} {track?.key_signature || 'N/A'}
</p>
</div>
<button onClick={() => deleteTrack(track.id)} className="p-2 hover:bg-white/5 rounded-full text-gray-600 hover:text-red-500 transition-all">
<BaseIcon path={mdiDelete} size={18} />
</button>
</div>
<div className="mt-4 flex items-center justify-between text-gray-600">
<div className="flex gap-4">
<button className="hover:text-white"><BaseIcon path={mdiHeart} size={18} /></button>
<button className="hover:text-white"><BaseIcon path={mdiShare} size={18} /></button>
</div>
<div className="text-[10px] font-mono">{new Date(track.createdAt).toLocaleDateString()}</div>
</div>
</div>
))
) : (
<div className="col-span-full py-20 flex flex-col items-center text-gray-600">
<BaseIcon path={mdiLibrary} size={64} className="mb-4 opacity-20" />
<p className="font-black uppercase italic tracking-widest">Sua biblioteca está vazia</p>
<button onClick={() => setActiveTab('create')} className="mt-4 text-[#00E5FF] text-[10px] font-black uppercase hover:underline">Comece a criar agora</button>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-gray-600 space-y-4">
<BaseIcon path={mdiEarth} size={64} className="opacity-20" />
<p className="font-black uppercase italic tracking-widest">Explore hits da comunidade</p>
<span className="text-[10px] uppercase font-bold px-4 py-1 bg-white/5 rounded-full">Em Breve</span>
</div>
)}
</div>
</main>
</div>
{/* Global Player */}
{currentTrack && (
<div className="h-[120px] bg-[#0A0A0A]/95 border-t border-white/5 flex items-center px-8 z-[200]">
<div className="flex items-center w-[300px] gap-4">
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-[#00E5FF] to-purple-600 flex items-center justify-center flex-shrink-0 relative group">
<BaseIcon path={mdiMusic} size={32} className="text-white" />
<button
onClick={() => openLyrics(currentTrack)}
className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-all flex items-center justify-center rounded-xl"
>
<BaseIcon path={mdiTextBoxOutline} size={20} />
</button>
</div>
<div className="min-w-0">
<h4 className="text-white font-black text-lg uppercase italic truncate">{currentTrack?.title}</h4>
<p className="text-[10px] font-bold text-gray-500 uppercase truncate">
{currentTrack?.ai_data?.mood || 'AI Professional Production'}
</p>
</div>
</div>
<div className="flex-1 flex flex-col items-center max-w-2xl mx-auto">
<div className="flex items-center gap-6 mb-2">
<button className="text-gray-500 hover:text-white transition-all"><BaseIcon path={mdiSkipPrevious} size={28} /></button>
<button
onClick={togglePlayback}
className="w-12 h-12 rounded-full bg-white flex items-center justify-center text-black hover:scale-110 transition-all"
>
<BaseIcon path={isPlaying ? mdiPause : mdiPlay} size={28} />
</button>
<button className="text-gray-500 hover:text-white transition-all"><BaseIcon path={mdiSkipNext} size={28} /></button>
</div>
<div className="w-full flex items-center gap-3">
<span className="text-[10px] font-mono text-gray-500 w-10 text-right">{formatTime(currentTime)}</span>
<input
type="range"
min="0"
max="100"
value={progress}
onChange={handleSeek}
className="flex-1 h-1 bg-white/10 rounded-full appearance-none cursor-pointer accent-[#00E5FF]"
/>
<span className="text-[10px] font-mono text-gray-500 w-10">{formatTime(duration)}</span>
</div>
</div>
<div className="w-[300px] flex justify-end items-center gap-4">
<button
onClick={() => handleDownload(currentTrack)}
className="p-2 hover:bg-white/5 rounded-full text-gray-500 hover:text-[#00E5FF] transition-all flex items-center gap-2"
title="Baixar MP3/MP4"
>
<BaseIcon path={mdiDownload} size={20} />
<span className="text-[10px] font-black uppercase hidden lg:block">Baixar</span>
</button>
<div className="flex items-center gap-2">
<BaseIcon path={volume === 0 ? mdiVolumeMute : volume < 0.5 ? mdiVolumeMedium : mdiVolumeHigh} size={20} className="text-gray-500" />
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => {
const val = parseFloat(e.target.value);
setVolume(val);
if (audioRef.current) audioRef.current.volume = val;
}}
className="w-24 h-1 bg-white/10 rounded-full appearance-none cursor-pointer accent-[#00E5FF]"
/>
</div>
<button className="p-2 hover:bg-white/5 rounded-full text-gray-500 hover:text-white">
<BaseIcon path={mdiDotsVertical} size={20} />
</button>
</div>
<audio ref={audioRef} onTimeUpdate={handleTimeUpdate} className="hidden" />
</div>
)}
{/* Lyrics Modal with Synchronized Highlights */}
{showLyricsModal && lyricsToView && (
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/90 backdrop-blur-xl" onClick={() => setShowLyricsModal(false)} />
<div className="relative bg-[#0D0D0D] border border-white/10 rounded-[40px] w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col shadow-2xl">
<header className="p-8 border-b border-white/5 flex justify-between items-center">
<div>
<h2 className="text-2xl font-black uppercase italic text-[#00E5FF]">{lyricsToView.title}</h2>
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest mt-1">
{lyricsToView.ai_data?.style} {lyricsToView.ai_data?.language} {lyricsToView.ai_data?.mood}
</p>
</div>
<button onClick={() => setShowLyricsModal(false)} className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/10 transition-all">
<BaseIcon path={mdiClose} size={24} />
</button>
</header>
<div className="flex-1 overflow-y-auto p-8 aside-scrollbars space-y-8">
{lyricsToView.ai_data?.synchronized_lyrics && lyricsToView.ai_data.synchronized_lyrics.length > 0 ? (
<div className="space-y-4">
{lyricsToView.ai_data.synchronized_lyrics.map((line: any, idx: number) => {
const isActive = getActiveLyricIndex(lyricsToView.ai_data.synchronized_lyrics) === idx;
return (
<div
key={idx}
className={`transition-all duration-500 transform ${isActive ? 'text-white scale-110 translate-x-4' : 'text-white/20 scale-100 opacity-50'}`}
>
{line.type && line.type !== 'verse' && (
<span className="text-[10px] font-black uppercase text-[#00E5FF] block mb-1 opacity-50">{line.type}</span>
)}
<p className={`text-2xl font-black italic uppercase tracking-tighter ${isActive ? 'text-[#00E5FF]' : ''}`}>
{line.text}
</p>
</div>
);
})}
</div>
) : lyricsToView.ai_data?.lyrics ? (
Object.entries(lyricsToView.ai_data.lyrics).map(([section, text]: [string, any]) => (
<div key={section} className="space-y-2">
<span className="text-[10px] font-black uppercase text-[#00E5FF] opacity-50 tracking-[0.2em]">{section}</span>
<p className="text-lg font-medium leading-relaxed whitespace-pre-wrap">{text}</p>
</div>
))
) : (
<p className="text-gray-500 italic">Nenhuma letra disponível para esta faixa instrumental.</p>
)}
</div>
<footer className="p-8 border-t border-white/5 bg-black/20">
<button
onClick={() => {
playTrack(lyricsToView);
}}
className="w-full py-4 bg-white text-black rounded-2xl font-black uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-3"
>
<BaseIcon path={currentTrack?.id === lyricsToView.id && isPlaying ? mdiPause : mdiPlay} size={24} />
{currentTrack?.id === lyricsToView.id && isPlaying ? 'Pausar' : 'Tocar e Sincronizar'}
</button>
</footer>
</div>
</div>
)}
<style jsx global>{`
.aside-scrollbars::-webkit-scrollbar {
width: 4px;
}
.aside-scrollbars::-webkit-scrollbar-track {
background: transparent;
}
.aside-scrollbars::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.aside-scrollbars::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
input[type='range']::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
background: #00E5FF;
border-radius: 50%;
cursor: pointer;
}
`}</style>
</div>
);
};
SunoStudio.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default SunoStudio;

View File

@ -25,6 +25,21 @@ const initialState: MainState = {
export const resetAction = createAction('auth/passwordReset/reset') export const resetAction = createAction('auth/passwordReset/reset')
export const loginAdmin = createAsyncThunk(
'auth/loginAdmin',
async (payload: { key: string }, { rejectWithValue }) => {
try {
const response = await axios.post('auth/signin/admin', payload);
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const loginUser = createAsyncThunk( export const loginUser = createAsyncThunk(
'auth/loginUser', 'auth/loginUser',
async (creds: Record<string, string>, { rejectWithValue }) => { async (creds: Record<string, string>, { rejectWithValue }) => {
@ -40,6 +55,36 @@ export const loginUser = createAsyncThunk(
} }
); );
export const loginWithCode = createAsyncThunk(
'auth/loginWithCode',
async (creds: { code: string }, { rejectWithValue }) => {
try {
const response = await axios.post('auth/signin/code', creds);
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const signupUser = createAsyncThunk(
'auth/signupUser',
async (creds: Record<string, string>, { rejectWithValue }) => {
try {
const response = await axios.post('auth/signup', creds);
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const passwordReset = createAsyncThunk( export const passwordReset = createAsyncThunk(
'auth/passwordReset', 'auth/passwordReset',
async (value: Record<string, string>, { rejectWithValue }) => { async (value: Record<string, string>, { rejectWithValue }) => {
@ -79,10 +124,7 @@ export const authSlice = createSlice({
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => { const handleAuthFulfilled = (state, action) => {
state.isFetching = true;
});
builder.addCase(loginUser.fulfilled, (state, action) => {
const token = action.payload; const token = action.payload;
const user = jwt.decode(token); const user = jwt.decode(token);
@ -91,12 +133,45 @@ export const authSlice = createSlice({
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;
state.isFetching = false;
};
builder.addCase(loginAdmin.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginAdmin.fulfilled, handleAuthFulfilled);
builder.addCase(loginAdmin.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Invalid Admin Key';
state.isFetching = false;
}); });
builder.addCase(loginUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginUser.fulfilled, handleAuthFulfilled);
builder.addCase(loginUser.rejected, (state, action) => { builder.addCase(loginUser.rejected, (state, action) => {
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(loginWithCode.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginWithCode.fulfilled, handleAuthFulfilled);
builder.addCase(loginWithCode.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Invalid access code';
state.isFetching = false;
});
builder.addCase(signupUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(signupUser.fulfilled, handleAuthFulfilled);
builder.addCase(signupUser.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
state.isFetching = false;
});
builder.addCase(findMe.pending, () => { builder.addCase(findMe.pending, () => {
console.log('Pending findMe'); console.log('Pending findMe');
}); });