This commit is contained in:
Flatlogic Bot 2026-03-01 20:26:04 +00:00
parent e56befb06a
commit 14794ed687
10 changed files with 728 additions and 323 deletions

View File

@ -62,6 +62,35 @@ router.post('/signin/local', wrapAsync(async (req, res) => {
res.status(200).send(payload);
}));
/**
* @swagger
* /api/auth/signin/code:
* post:
* tags: [Auth]
* summary: Logs user into the system using access code
* description: Logs user into the system using access code
* requestBody:
* description: Set valid access code
* content:
* application/json:
* schema:
* type: object
* required:
* - code
* properties:
* code:
* type: string
* responses:
* 200:
* description: Successful login
* 400:
* description: Invalid code supplied
*/
router.post('/signin/code', wrapAsync(async (req, res) => {
const payload = await AuthService.signinWithCode(req.body.code);
res.status(200).send(payload);
}));
/**
* @swagger
* /api/auth/me:
@ -204,4 +233,4 @@ function socialRedirect(res, state, token, config) {
res.redirect(config.uiUrl + "/login?token=" + token);
}
module.exports = router;
module.exports = router;

View File

@ -1,4 +1,5 @@
const UsersDBApi = require('../db/api/users');
const Access_codesDBApi = require('../db/api/access_codes');
const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt');
@ -8,6 +9,7 @@ const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email');
const config = require('../config');
const helpers = require('../helpers');
const db = require('../db/models');
class Auth {
static async signup(email, password, options = {}, host) {
@ -18,6 +20,8 @@ class Auth {
config.bcrypt.saltRounds,
);
let currentUser;
if (user) {
if (user.authenticationUid) {
throw new ValidationError(
@ -37,44 +41,40 @@ class Auth {
options,
);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
user.email,
host,
);
}
const data = {
user: {
id: user.id,
email: user.email
}
};
return helpers.jwtSign(data);
currentUser = user;
} else {
currentUser = await UsersDBApi.createFromAuth(
{
firstName: email.split('@')[0],
password: hashedPassword,
email: email,
},
options,
);
}
const newUser = await UsersDBApi.createFromAuth(
{
firstName: email.split('@')[0],
password: hashedPassword,
email: email,
},
options,
);
// Generate Access Code
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
await Access_codesDBApi.create({
code,
status: 'active',
user: currentUser.id,
max_uses: 1000,
uses_count: 0
}, options);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
newUser.email,
currentUser.email,
host,
);
}
const data = {
user: {
id: newUser.id,
email: newUser.email
id: currentUser.id,
email: currentUser.email,
accessCode: code // Return the code so the user knows it
}
};
@ -133,6 +133,35 @@ class Auth {
return helpers.jwtSign(data);
}
static async signinWithCode(code) {
const accessCode = await Access_codesDBApi.findBy({ code, status: 'active' });
if (!accessCode || !accessCode.user) {
throw new ValidationError('auth.invalidCode');
}
const user = await UsersDBApi.findBy({ id: accessCode.user.id });
if (!user || user.disabled) {
throw new ValidationError('auth.userDisabled');
}
// Update uses count
await Access_codesDBApi.update(accessCode.id, {
uses_count: (accessCode.uses_count || 0) + 1,
used_at: new Date()
}, { currentUser: user });
const data = {
user: {
id: user.id,
email: user.email
}
};
return helpers.jwtSign(data);
}
static async sendEmailAddressVerificationEmail(
email,
host,
@ -309,4 +338,4 @@ class Auth {
}
}
module.exports = Auth;
module.exports = Auth;

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 { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
}
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
}
}

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
</div>
</div>
)
}
}

View File

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

View File

@ -1,166 +1,112 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import { ReactElement } from 'react';
import Head from 'next/head';
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 BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import SectionFullScreen from '../components/SectionFullScreen';
import { mdiMusic, mdiMicrophone, mdiPiano, mdiChartTimelineVariant } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
export default function Starter() {
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>)
}
};
export default function Home() {
const title = "AI Music Studio";
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>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Home')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: 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'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Studio Musical Web app!"/>
<div className="space-y-3">
<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>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className="bg-[#121212] min-h-screen text-white font-sans overflow-x-hidden">
{/* Navigation */}
<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">
<div className="flex items-center space-x-2">
<BaseIcon path={mdiMusic} size={32} className="text-[#00E5FF]" />
<span className="text-2xl font-bold tracking-tight">{title}</span>
</div>
<div className="hidden md:flex space-x-8 items-center font-medium">
<a href="#features" className="hover:text-[#00E5FF] transition-colors">Features</a>
<a href="#instruments" className="hover:text-[#00E5FF] transition-colors">Instruments</a>
<Link href="/login" className="px-5 py-2 rounded-full border border-[#00E5FF] text-[#00E5FF] hover:bg-[#00E5FF] hover:text-black transition-all">
Login
</Link>
<Link href="/register" className="px-5 py-2 rounded-full bg-[#00E5FF] text-black hover:bg-white transition-all shadow-[0_0_15px_rgba(0,229,255,0.4)]">
Start Creating
</Link>
</div>
</nav>
{/* Hero Section */}
<SectionFullScreen bg="dark" className="relative flex flex-col items-center justify-center pt-20 pb-32">
<div className="absolute top-0 left-0 w-full h-full overflow-hidden z-0 pointer-events-none opacity-20">
<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]">
AI-Powered Music Composition
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-black mb-8 leading-tight tracking-tighter">
CREATE YOUR <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#00E5FF] to-[#BB86FC]">SYMPHONY</span> IN SECONDS
</h1>
<p className="text-xl md:text-2xl text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
From Sertanejo to Hip-Hop. Professional instruments, intelligent beats, and AI lyrics generator &mdash; all with just your access code.
</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="/register" 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)]">
Launch Studio
</Link>
<Link href="/login" className="w-full sm:w-auto px-10 py-5 rounded-xl bg-transparent border-2 border-white text-white text-xl font-bold hover:bg-white hover:text-black transition-all">
Enter with Code
</Link>
</div>
</div>
</SectionFullScreen>
</BaseButtons>
</CardBox>
{/* Features Section */}
<div id="features" className="py-32 px-6 md:px-12 bg-[#0A0A0A]">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<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={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>
</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={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>
</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>
{/* Call to Action */}
<div className="py-32 px-6 relative overflow-hidden">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full bg-gradient-to-r from-blue-900/20 to-purple-900/20 z-0"></div>
<div className="max-w-4xl mx-auto text-center relative z-10">
<h2 className="text-4xl md:text-6xl font-black mb-8 italic uppercase tracking-tighter">Your music journey starts with one code.</h2>
<p className="text-xl text-gray-400 mb-12 italic">Join thousands of creators building the future of sound.</p>
<Link href="/register" className="px-12 py-6 rounded-full bg-white text-black text-2xl font-black hover:bg-[#00E5FF] transition-all uppercase tracking-widest shadow-2xl">
Get Started Now
</Link>
</div>
</div>
{/* Footer */}
<footer className="py-12 px-6 border-t border-gray-900 text-center text-gray-600">
<p>© 2026 {title}. The World&apos;s Most Advanced AI Music Studio.</p>
</footer>
</div>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,12 +1,10 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js';
import { mdiInformation, mdiEye, mdiEyeOff, mdiKey, mdiEmail } from '@mdi/js';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
@ -16,7 +14,7 @@ import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import { findMe, loginUser, resetAction } from '../stores/authSlice';
import { findMe, loginUser, loginWithCode, resetAction } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify";
@ -28,13 +26,14 @@ export default function Login() {
const textColor = useAppSelector((state) => state.style.linkColor);
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const notify = (type, msg) => toast(msg, { type });
const [loginMethod, setLoginMethod] = useState<'email' | 'code'>('code');
const [ illustrationImage, setIllustrationImage ] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right');
const [showPassword, setShowPassword] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
@ -44,7 +43,7 @@ export default function Login() {
password: '8e470127',
remember: true })
const title = 'Studio Musical Web'
const title = 'AI Music Studio'
// Fetch Pexels image/video
useEffect( () => {
@ -88,8 +87,12 @@ export default function Login() {
};
const handleSubmit = async (value) => {
const {remember, ...rest} = value
await dispatch(loginUser(rest));
if (loginMethod === 'email') {
const {remember, ...rest} = value
await dispatch(loginUser(rest));
} else {
await dispatch(loginWithCode({ code: value.code }));
}
};
const setLogin = (target: HTMLElement) => {
@ -143,7 +146,7 @@ export default function Login() {
};
return (
<div style={contentPosition === 'background' ? {
<div className="bg-[#121212]" style={contentPosition === 'background' ? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
@ -157,101 +160,113 @@ export default function Login() {
<title>{getPageTitle('Login')}</title>
</Head>
<SectionFullScreen bg='violet'>
<SectionFullScreen bg='dark'>
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : 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'>
<h2 className="text-4xl font-semibold my-4">{title}</h2>
<div className='flex flex-row justify-between'>
<div>
<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 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">THE FUTURE OF SOUND</p>
</div>
<CardBox className='w-full md:w-3/5 lg:w-2/3 bg-gray-900 border-gray-800 shadow-2xl'>
<div className="flex mb-8 bg-gray-800 rounded-xl p-1">
<button
onClick={() => setLoginMethod('code')}
className={`flex-1 flex items-center justify-center space-x-2 py-3 rounded-lg font-bold transition-all ${loginMethod === 'code' ? 'bg-[#00E5FF] text-black shadow-lg' : 'text-gray-400 hover:text-white'}`}
>
<BaseIcon path={mdiKey} size={20} />
<span>ACCESS CODE</span>
</button>
<button
onClick={() => setLoginMethod('email')}
className={`flex-1 flex items-center justify-center space-x-2 py-3 rounded-lg font-bold transition-all ${loginMethod === 'email' ? 'bg-[#00E5FF] text-black shadow-lg' : 'text-gray-400 hover:text-white'}`}
>
<BaseIcon path={mdiEmail} size={20} />
<span>EMAIL LOGIN</span>
</button>
</div>
</CardBox>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<Formik
initialValues={initialValues}
initialValues={{
...initialValues,
code: ''
}}
enableReinitialize
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField
label='Login'
help='Please enter your login'>
<Field name='email' />
</FormField>
{loginMethod === 'email' ? (
<>
<FormField
label='Login'
help='Please enter your email'>
<Field name='email' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
<Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField>
<div
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
onClick={togglePasswordVisibility}
>
<BaseIcon
className='text-gray-500 hover:text-gray-700'
size={20}
path={showPassword ? mdiEyeOff : mdiEye}
/>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
<Field name='password' type={showPassword ? 'text' : 'password'} className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<div
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
onClick={togglePasswordVisibility}
>
<BaseIcon
className='text-gray-500 hover:text-[#00E5FF]'
size={20}
path={showPassword ? mdiEyeOff : mdiEye}
/>
</div>
</div>
<div className={'flex justify-between mb-6'}>
<FormCheckRadio type='checkbox' label='Remember me'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`text-[#00E5FF] font-bold hover:underline`} href={'/forgot'}>
Forgot password?
</Link>
</div>
</>
) : (
<div className="mb-8">
<FormField
label='Access Code'
help='Enter the code generated for your account'>
<Field
name='code'
placeholder="XXXXXX"
className="bg-gray-800 border-gray-700 text-white text-3xl text-center font-black tracking-widest placeholder-gray-600 focus:ring-[#00E5FF] focus:border-[#00E5FF] rounded-xl py-6"
/>
</FormField>
<p className="text-sm text-gray-500 mt-4 italic text-center">
Each creator has a unique system-generated code.
</p>
</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>
<BaseButton
className={'w-full'}
className={'w-full py-4 text-xl font-black rounded-xl'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
label={isFetching ? 'ENTERING...' : 'ENTER STUDIO'}
color='info'
disabled={isFetching}
/>
</BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
<BaseDivider className="border-gray-800" />
<p className={'text-center text-gray-400 font-medium'}>
New to the studio?{' '}
<Link className={`text-[#00E5FF] font-black hover:underline`} href={'/register'}>
Generate New Code
</Link>
</p>
</Form>
@ -260,17 +275,17 @@ export default function Login() {
</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/'>
<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 className="text-white font-bold">{title}</span>. Built for Creators.</p>
<Link className='py-6 ml-4 text-sm hover:text-white' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<ToastContainer />
<ToastContainer theme="dark" />
</div>
);
}
Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head';
@ -12,78 +12,138 @@ import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import BaseIcon from '../components/BaseIcon';
import { mdiMusic, mdiContentCopy, mdiCheckDecagram } from '@mdi/js';
import Link from 'next/link';
import axios from "axios";
import jwt from 'jsonwebtoken';
export default function Register() {
const [loading, setLoading] = React.useState(false);
const [accessCode, setAccessCode] = useState<string | null>(null);
const router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: token } = await axios.post('/auth/signup', value);
const decoded: any = jwt.decode(token);
const { data: response } = await axios.post('/auth/signup',value);
await router.push('/login')
if (decoded && decoded.user && decoded.user.accessCode) {
setAccessCode(decoded.user.accessCode);
notify('success', 'Account created! Here is your access code.');
} else {
await router.push('/login')
notify('success', 'Account created! Please login.')
}
setLoading(false)
notify('success', 'Please check your email for verification link')
} catch (error) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
notify('error', 'Something went wrong. Try again')
}
};
const copyToClipboard = () => {
if (accessCode) {
navigator.clipboard.writeText(accessCode);
notify('info', 'Code copied to clipboard!');
}
}
return (
<>
<div className="bg-[#121212] min-h-screen">
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('Register')}</title>
</Head>
<SectionFullScreen bg='violet'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
<Formik
initialValues={{
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>
<SectionFullScreen bg='dark'>
<div className="flex flex-col items-center justify-center w-full px-4">
<div className="text-center mb-8">
<div className="flex items-center justify-center space-x-2 mb-2">
<BaseIcon path={mdiMusic} size={40} className="text-[#00E5FF]" />
<h2 className="text-4xl font-black text-white tracking-tighter uppercase italic">AI Music Studio</h2>
</div>
<p className="text-gray-400 font-bold">JOIN THE CREATIVE REVOLUTION</p>
</div>
<BaseDivider />
{!accessCode ? (
<CardBox className='w-full md:w-3/5 lg:w-1/3 xl:w-1/4 bg-gray-900 border-gray-800 shadow-2xl'>
<Formik
initialValues={{
email: '',
password: '',
confirm: ''
}}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Your creative identity'>
<Field type='email' name='email' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<FormField label='Password' help='Keep your studio secure'>
<Field type='password' name='password' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<FormField label='Confirm Password' help='Just to be sure'>
<Field type='password' name='confirm' className="bg-gray-800 border-gray-700 text-white focus:ring-[#00E5FF]" />
</FormField>
<BaseButtons>
<BaseDivider className="border-gray-800" />
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'CREATING...' : 'GENERATE ACCESS CODE' }
color='info'
className="w-full py-4 font-black rounded-xl shadow-[0_0_15px_rgba(0,229,255,0.3)] hover:scale-105 transition-transform"
/>
</BaseButtons>
<div className="mt-8 text-center">
<p className="text-gray-400">
Already have a code?{' '}
<Link href="/login" className="text-[#00E5FF] font-black hover:underline">
Login Here
</Link>
</p>
</div>
</Form>
</Formik>
</CardBox>
) : (
<CardBox className='w-full md:w-3/5 lg:w-1/3 xl:w-1/4 bg-gray-900 border-gray-800 shadow-2xl text-center py-10'>
<BaseIcon path={mdiCheckDecagram} size={64} className="text-[#00E5FF] mx-auto mb-6" />
<h3 className="text-2xl font-black text-white mb-2 uppercase italic tracking-tighter">Your Access Code</h3>
<p className="text-gray-400 mb-8">Save this code. You&apos;ll need it to enter your studio.</p>
<div className="bg-black border border-gray-800 rounded-2xl p-8 mb-8 relative group cursor-pointer" onClick={copyToClipboard}>
<div className="text-5xl font-black text-white tracking-[0.2em] mb-4">{accessCode}</div>
<div className="text-[#00E5FF] flex items-center justify-center space-x-2 opacity-60 group-hover:opacity-100 transition-opacity">
<BaseIcon path={mdiContentCopy} size={20} />
<span className="text-sm font-bold uppercase">Click to Copy</span>
</div>
</div>
<BaseButtons className="flex-col space-y-4">
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
color='info'
href="/login"
label="ENTER STUDIO NOW"
color="info"
className="w-full py-4 font-black rounded-xl text-xl shadow-[0_0_20px_rgba(0,229,255,0.4)]"
/>
<Link href="/register" onClick={() => setAccessCode(null)} className="text-gray-500 hover:text-white font-bold transition-colors">
Create another account
</Link>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</CardBox>
)}
</div>
</SectionFullScreen>
<ToastContainer />
</>
<ToastContainer theme="dark" />
</div>
);
}

View File

@ -0,0 +1,272 @@
import {
mdiMusic,
mdiRobotOutline,
mdiPlus,
mdiPlay,
mdiPiano,
mdiGuitarAcoustic,
mdiMicrophone,
mdiTuneVariant
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import BaseIcon from '../components/BaseIcon';
import BaseButton from '../components/BaseButton';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import axios from 'axios';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { aiResponse } from '../stores/openAiSlice';
import { toast, ToastContainer } from 'react-toastify';
const Studio = () => {
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const { isAskingResponse, aiResponse: gptResult } = useAppSelector((state) => state.openAi);
const [prompt, setPrompt] = useState('');
const [style, setStyle] = useState('Pop');
const [projects, setProjects] = useState([]);
const [instruments, setInstruments] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchStudioData();
}, []);
const fetchStudioData = async () => {
try {
setLoading(true);
const [projRes, instRes] = await Promise.all([
axios.get('/projects?limit=5'),
axios.get('/instruments?limit=8')
]);
setProjects(projRes.data.rows || []);
setInstruments(instRes.data.rows || []);
} catch (error) {
console.error('Error fetching studio data:', error);
} finally {
setLoading(false);
}
};
const handleAiGenerate = async () => {
if (!prompt) {
toast.error('Please enter an idea for your song');
return;
}
const fullPrompt = `Create a song structure based on this idea: "${prompt}". Style: ${style}. Return a JSON with title, lyrics (verses, chorus), and suggested instruments.`;
dispatch(aiResponse({
input: [
{ role: 'system', content: 'You are a professional music producer and songwriter.' },
{ role: 'user', content: fullPrompt },
],
options: { poll_interval: 5, poll_timeout: 300 },
}));
};
useEffect(() => {
if (gptResult) {
toast.success('AI has generated your song structure!');
// In a real app, we would save this as a new project
}
}, [gptResult]);
return (
<>
<Head>
<title>{getPageTitle('Musical Studio')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiMusic} title='Musical Studio' main>
<BaseButton
label="New Project"
color="info"
icon={mdiPlus}
onClick={() => toast.info('Manual project creation coming soon!')}
/>
</SectionTitleLineWithButton>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* AI Generator Section */}
<div className="lg:col-span-2 space-y-6">
<CardBox className="bg-gradient-to-br from-gray-900 to-indigo-900 border-none shadow-2xl overflow-hidden relative">
<div className="absolute top-0 right-0 p-4 opacity-10">
<BaseIcon path={mdiRobotOutline} size={120} />
</div>
<div className="relative z-10">
<h2 className="text-2xl font-bold text-white mb-4 flex items-center">
<BaseIcon path={mdiRobotOutline} className="mr-2 text-[#00E5FF]" />
AI SONGWRITER
</h2>
<p className="text-indigo-200 mb-6">Describe your idea, choose a style, and let AI build the foundation of your next hit.</p>
<div className="space-y-4">
<FormField label="Your Song Idea" labelColor="text-white">
<textarea
className="w-full bg-black/40 border-gray-700 text-white rounded-xl p-4 focus:ring-[#00E5FF]"
placeholder="e.g. A romantic song about a summer night in Ibiza..."
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</FormField>
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<div className="flex-1">
<FormField label="Musical Style" labelColor="text-white">
<select
className="w-full bg-black/40 border-gray-700 text-white rounded-xl p-3 focus:ring-[#00E5FF]"
value={style}
onChange={(e) => setStyle(e.target.value)}
>
<option>Pop</option>
<option>Rock</option>
<option>Sertanejo</option>
<option>Hip-Hop</option>
<option>Jazz</option>
<option>Electronic</option>
<option>MPB</option>
</select>
</FormField>
</div>
<div className="flex items-end">
<BaseButton
label={isAskingResponse ? "COMPOSING..." : "GENERATE SONG"}
color="info"
className="w-full md:w-auto px-8 py-3 font-bold rounded-xl shadow-[0_0_15px_rgba(0,229,255,0.4)]"
disabled={isAskingResponse}
onClick={handleAiGenerate}
/>
</div>
</div>
</div>
</div>
</CardBox>
{gptResult && (
<CardBox className="border-[#00E5FF]/30 bg-black/40 backdrop-blur-md animate-fade-in">
<h3 className="text-xl font-bold text-[#00E5FF] mb-4 uppercase tracking-wider italic">Generated Structure</h3>
<div className="prose prose-invert max-w-none">
<pre className="whitespace-pre-wrap font-mono text-sm bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
{typeof gptResult === 'string' ? gptResult : JSON.stringify(gptResult, null, 2)}
</pre>
</div>
<div className="mt-6 flex space-x-4">
<BaseButton label="Create Project from this" color="success" icon={mdiPlus} />
<BaseButton label="Refine" color="white" outline />
</div>
</CardBox>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<CardBox title="Recent Projects" icon={mdiMusic}>
{projects.length > 0 ? (
<div className="space-y-4">
{projects.map((project: any) => (
<div key={project.id} className="flex items-center justify-between p-3 bg-gray-800/50 rounded-xl hover:bg-gray-800 transition-colors cursor-pointer group">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-900 rounded-lg flex items-center justify-center">
<BaseIcon path={mdiMusic} className="text-indigo-400" />
</div>
<div>
<div className="font-bold text-sm">{project.name || 'Untitled Project'}</div>
<div className="text-xs text-gray-500">{project.genre?.name || 'No Genre'}</div>
</div>
</div>
<BaseIcon path={mdiPlay} className="text-gray-600 group-hover:text-[#00E5FF] transition-colors" />
</div>
))}
<BaseButton href="/projects/projects-list" label="View All Projects" color="info" className="w-full" outline />
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p>No projects yet. Start by generating one!</p>
</div>
)}
</CardBox>
<CardBox title="Your Instruments" icon={mdiPiano}>
<div className="grid grid-cols-2 gap-3">
{instruments.map((inst: any) => (
<div key={inst.id} className="p-3 bg-gray-800/50 rounded-xl flex flex-col items-center text-center hover:bg-gray-800 transition-colors cursor-pointer">
<BaseIcon path={mdiPiano} className="text-[#03DAC6] mb-2" />
<span className="text-xs font-medium">{inst.name}</span>
</div>
))}
<div className="p-3 bg-gray-900 border border-dashed border-gray-700 rounded-xl flex flex-col items-center text-center justify-center text-gray-500 hover:text-white hover:border-white transition-all cursor-pointer">
<BaseIcon path={mdiPlus} size={20} />
<span className="text-[10px] uppercase font-bold mt-1">Add More</span>
</div>
</div>
</CardBox>
</div>
</div>
{/* Sidebar / Quick Settings */}
<div className="space-y-6">
<CardBox title="Studio Status" icon={mdiTuneVariant}>
<div className="space-y-6">
<div>
<div className="flex justify-between text-xs font-bold uppercase tracking-widest text-gray-500 mb-2">
<span>Master Volume</span>
<span>85%</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-[#00E5FF] to-[#BB86FC] w-[85%]"></div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-center">
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700">
<div className="text-2xl font-black text-white">128</div>
<div className="text-[10px] text-gray-500 uppercase font-bold">BPM</div>
</div>
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700">
<div className="text-2xl font-black text-white">4/4</div>
<div className="text-[10px] text-gray-500 uppercase font-bold">Time</div>
</div>
</div>
<BaseDivider />
<div className="space-y-3">
<h4 className="text-xs font-black text-gray-500 uppercase">Active Effects</h4>
<div className="flex flex-wrap gap-2">
{['Reverb', 'Compressor', 'Delay'].map(fx => (
<span key={fx} className="px-3 py-1 bg-indigo-900/30 text-indigo-400 border border-indigo-900 rounded-full text-[10px] font-bold">
{fx}
</span>
))}
</div>
</div>
</div>
</CardBox>
<CardBox className="bg-gradient-to-br from-[#00E5FF] to-[#BB86FC] p-0 border-none overflow-hidden">
<div className="p-6 text-black">
<h3 className="text-xl font-black mb-2 italic">GO PRO</h3>
<p className="text-sm font-medium mb-4">Unlock unlimited AI generations and 500+ premium instruments.</p>
<BaseButton label="Upgrade Now" color="white" className="w-full font-black rounded-xl" />
</div>
</CardBox>
</div>
</div>
</SectionMain>
<ToastContainer theme="dark" />
</>
);
};
Studio.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default Studio;

View File

@ -40,6 +40,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(
'auth/passwordReset',
async (value: Record<string, string>, { rejectWithValue }) => {
@ -79,10 +109,7 @@ export const authSlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginUser.fulfilled, (state, action) => {
const handleAuthFulfilled = (state, action) => {
const token = action.payload;
const user = jwt.decode(token);
@ -91,12 +118,36 @@ export const authSlice = createSlice({
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
});
state.isFetching = false;
};
builder.addCase(loginUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(loginUser.fulfilled, handleAuthFulfilled);
builder.addCase(loginUser.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
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, () => {
console.log('Pending findMe');
});
@ -121,4 +172,4 @@ export const authSlice = createSlice({
// Action creators are generated for each case reducer function
export const { logoutUser } = authSlice.actions;
export default authSlice.reducer;
export default authSlice.reducer;