diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index d6f29e8..addaaa4 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -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; \ No newline at end of file diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js index 2862da4..6b348e4 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.js @@ -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; \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index eb155e3..1986306 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -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
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -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({ ) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 126ebfa..12ee19d 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -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 \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index acbd0b8..e886ab1 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; +export default function Home() { + const title = "AI Music Studio"; + return ( -
+ <> - {getPageTitle('Starter Page')} + {getPageTitle('Home')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+
+ {/* Navigation */} + + + {/* Hero Section */} + +
+
+
+
+ +
+
+ AI-Powered Music Composition
- - - +

+ CREATE YOUR SYMPHONY IN SECONDS +

+

+ From Sertanejo to Hip-Hop. Professional instruments, intelligent beats, and AI lyrics generator — all with just your access code. +

+
+ + Launch Studio + + + Enter with Code + +
+
+
- - + {/* Features Section */} +
+
+
+
+ +

AI Lyrics Engine

+

Input an idea, choose a style, and watch as our AI crafts a complete song structure with verses, chorus, and bridge.

+
+
+ +

World Instruments

+

Piano, Guitar, Accordion, Flute — access every instrument in the world with high-fidelity studio samples.

+
+
+ +

Beat Configurator

+

Customize rhythms, BPM, and swing. Perfect for any style from Sertanejo Raiz to Modern Hip-Hop.

+
+
+
-
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
-
+ {/* Call to Action */} +
+
+
+

Your music journey starts with one code.

+

Join thousands of creators building the future of sound.

+ + Get Started Now + +
+
+ + {/* Footer */} +
+

© 2026 {title}. The World's Most Advanced AI Music Studio.

+
+
+ ); } -Starter.getLayout = function getLayout(page: ReactElement) { +Home.getLayout = function getLayout(page: ReactElement) { return {page}; }; - diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 8df0ac1..658910c 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -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 ( -
{getPageTitle('Login')} - +
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null} {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null} -
+
- - -

{title}

- -
-
- -

Use{' '} - setLogin(e.target)}>admin@flatlogic.com{' / '} - 8e470127{' / '} - to login as Admin

-

Use setLogin(e.target)}>client@hello.com{' / '} - f04a8902244c{' / '} - to login as User

-
-
- -
+
+

{title}

+

THE FUTURE OF SOUND

+
+ + +
+ +
-
- - + handleSubmit(values)} >
- - - + {loginMethod === 'email' ? ( + <> + + + -
- - - -
- +
+ + + +
+ +
+
+ +
+ + + + + + Forgot password? + +
+ + ) : ( +
+ + + +

+ Each creator has a unique system-generated code. +

-
- -
- - - - - - Forgot password? - -
- - + )} -
-

- Don’t have an account yet?{' '} - - New Account + + + +

+ New to the studio?{' '} + + Generate New Code

@@ -260,17 +275,17 @@ export default function Login() {
-
-

© 2026 {title}. © All rights reserved

- +
+

© 2026 {title}. Built for Creators.

+ Privacy Policy
- +
); } Login.getLayout = function getLayout(page: ReactElement) { return {page}; -}; +}; \ No newline at end of file diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx index 73a3987..d245f78 100644 --- a/frontend/src/pages/register.tsx +++ b/frontend/src/pages/register.tsx @@ -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(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 ( - <> +
- {getPageTitle('Login')} + {getPageTitle('Register')} - - - handleSubmit(values)} - > -
- - - - - - - - - - + +
+
+
+ +

AI Music Studio

+
+

JOIN THE CREATIVE REVOLUTION

+
- + {!accessCode ? ( + + handleSubmit(values)} + > + + + + + + + + + + + - + + + + + + +
+

+ Already have a code?{' '} + + Login Here + +

+
+ +
+
+ ) : ( + + +

Your Access Code

+

Save this code. You'll need it to enter your studio.

+ +
+
{accessCode}
+
+ + Click to Copy +
+
+ + - + setAccessCode(null)} className="text-gray-500 hover:text-white font-bold transition-colors"> + Create another account + - - -
+ + )} +
- - + +
); } diff --git a/frontend/src/pages/studio.tsx b/frontend/src/pages/studio.tsx new file mode 100644 index 0000000..42ca860 --- /dev/null +++ b/frontend/src/pages/studio.tsx @@ -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 ( + <> + + {getPageTitle('Musical Studio')} + + + + toast.info('Manual project creation coming soon!')} + /> + + +
+ {/* AI Generator Section */} +
+ +
+ +
+
+

+ + AI SONGWRITER +

+

Describe your idea, choose a style, and let AI build the foundation of your next hit.

+ +
+ +