diff --git a/backend/src/config.js b/backend/src/config.js index 00de362..882072e 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -1,6 +1,3 @@ - - - const os = require('os'); const config = { @@ -14,6 +11,7 @@ const config = { admin_pass: "8e470127", user_pass: "f04a8902244c", admin_email: "admin@flatlogic.com", + admin_private_key: process.env.ADMIN_PRIVATE_KEY || 'studio-admin-key-9283-7465-1029', providers: { LOCAL: 'local', GOOGLE: 'google', @@ -76,4 +74,4 @@ config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; -module.exports = config; +module.exports = config; \ No newline at end of file diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index addaaa4..b34fec9 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,7 +1,6 @@ const express = require('express'); const passport = require('passport'); -const config = require('../config'); const AuthService = require('../services/auth'); const ForbiddenError = require('../services/notifications/errors/forbidden'); const EmailSender = require('../services/email'); @@ -9,104 +8,44 @@ const wrapAsync = require('../helpers').wrapAsync; 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 - * tags: - * name: Auth - * description: Authorization operations - */ - -/** - * @swagger - * /api/auth/signin/local: + * /api/auth/signin/admin: * post: * tags: [Auth] - * summary: Logs user into the system - * description: Logs user into the system + * summary: Logs admin into the system using private key + * description: Logs admin into the system using private key * requestBody: - * description: Set valid user email and password - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Auth" - * responses: - * 200: - * description: Successful login - * 400: - * description: Invalid username/password supplied - * x-codegen-request-body-name: body - */ - -router.post('/signin/local', wrapAsync(async (req, res) => { - const payload = await AuthService.signin(req.body.email, req.body.password, req,); - 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 + * description: Set valid admin private key * content: * application/json: * schema: * type: object * required: - * - code + * - key * properties: - * code: + * key: * type: string * responses: * 200: * description: Successful login * 400: - * description: Invalid code supplied + * description: Invalid key supplied */ -router.post('/signin/code', wrapAsync(async (req, res) => { - const payload = await AuthService.signinWithCode(req.body.code); +router.post('/signin/admin', wrapAsync(async (req, res) => { + const payload = await AuthService.signinWithAdminKey(req.body.key); res.status(200).send(payload); })); -/** - * @swagger - * /api/auth/me: - * get: - * security: - * - bearerAuth: [] - * tags: [Auth] - * summary: Get current authorized user info - * 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.post('/signin/local', wrapAsync(async () => { + // Disabled + throw new ForbiddenError('auth.signinDisabled'); +})); + +router.post('/signin/code', wrapAsync(async () => { + // Disabled + throw new ForbiddenError('auth.signinDisabled'); +})); router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => { if (!req.currentUser || !req.currentUser.id) { @@ -118,9 +57,8 @@ router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => res.status(200).send(payload); }); -router.put('/password-reset', wrapAsync(async (req, res) => { - const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,); - res.status(200).send(payload); +router.put('/password-reset', wrapAsync(async () => { + throw new ForbiddenError('auth.disabled'); })); router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { @@ -128,56 +66,16 @@ router.put('/password-update', passport.authenticate('jwt', {session: false}), w res.status(200).send(payload); })); -router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { - if (!req.currentUser) { - throw new ForbiddenError(); - } - - await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); - const payload = true; - res.status(200).send(payload); +router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async () => { + throw new ForbiddenError('auth.disabled'); })); -router.post('/send-password-reset-email', wrapAsync(async (req, res) => { - const link = new URL(req.headers.referer); - await AuthService.sendPasswordResetEmail(req.body.email, 'register', link.host,); - const payload = true; - res.status(200).send(payload); +router.post('/send-password-reset-email', wrapAsync(async () => { + throw new ForbiddenError('auth.disabled'); })); -/** - * @swagger - * /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.post('/signup', wrapAsync(async () => { + throw new ForbiddenError('auth.signupDisabled'); })); router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { @@ -190,9 +88,8 @@ router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync res.status(200).send(payload); })); -router.put('/verify-email', wrapAsync(async (req, res) => { - const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer) - res.status(200).send(payload); +router.put('/verify-email', wrapAsync(async () => { + throw new ForbiddenError('auth.disabled'); })); router.get('/email-configured', (req, res) => { @@ -200,37 +97,6 @@ router.get('/email-configured', (req, res) => { 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); -function socialRedirect(res, state, token, config) { - res.redirect(config.uiUrl + "/login?token=" + token); -} - module.exports = router; \ No newline at end of file diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js index 6b348e4..c70562a 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.js @@ -1,5 +1,4 @@ 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'); @@ -12,154 +11,42 @@ const helpers = require('../helpers'); const db = require('../db/models'); class Auth { - static async signup(email, password, options = {}, host) { - const user = await UsersDBApi.findBy({email}); - - const hashedPassword = await bcrypt.hash( - password, - config.bcrypt.saltRounds, - ); - - let currentUser; - - if (user) { - if (user.authenticationUid) { - throw new ValidationError( - 'auth.emailAlreadyInUse', - ); - } - - if (user.disabled) { - throw new ValidationError( - 'auth.userDisabled', - ); - } - - await UsersDBApi.updatePassword( - user.id, - hashedPassword, - options, - ); - - currentUser = user; - } else { - currentUser = 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( - currentUser.email, - host, - ); - } - - const data = { - user: { - id: currentUser.id, - email: currentUser.email, - accessCode: code // Return the code so the user knows it - } - }; - - return helpers.jwtSign(data); + static async signup() { + // Standard signup is disabled + throw new ForbiddenError('auth.signupDisabled'); } - static async signin(email, password, options = {}) { - const user = await UsersDBApi.findBy({email}); + static async signin() { + // Standard email login is disabled, use admin key + throw new ForbiddenError('auth.signinDisabled'); + } + + static async signinWithAdminKey(key) { + if (!key || key !== config.admin_private_key) { + throw new ValidationError('auth.invalidAdminKey'); + } + + // Always log in as the default admin + const adminEmail = config.admin_email; + const user = await UsersDBApi.findBy({ email: adminEmail }); 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', - ); + throw new ValidationError('auth.adminNotFound'); } const data = { user: { id: user.id, - email: user.email + email: user.email, + role: 'admin' } }; 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 signinWithCode() { + throw new ForbiddenError('auth.signinDisabled'); } static async sendEmailAddressVerificationEmail( @@ -260,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( newPassword, config.bcrypt.saltRounds, diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 8d45685..0815317 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -7,7 +7,15 @@ "loading": "Loading..." }, "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": { "loginLabel": "Login", @@ -37,7 +45,7 @@ "components": { "widgetCreator": { "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", "settingsDescription": "What role are we showing and creating widgets for?", "doneButton": "Done", @@ -49,4 +57,4 @@ "minLength": "Minimum length: {{count}} characters" } } -} +} \ No newline at end of file diff --git a/frontend/public/locales/pt/common.json b/frontend/public/locales/pt/common.json new file mode 100644 index 0000000..022b8b6 --- /dev/null +++ b/frontend/public/locales/pt/common.json @@ -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" + } + } +} \ No newline at end of file diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index f2f373a..dac03d1 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import Select, { components, SingleValueProps, OptionProps } from 'react-select'; +import { useTranslation } from 'react-i18next'; type LanguageOption = { label: string; value: string }; @@ -8,6 +9,7 @@ const LANGS: LanguageOption[] = [ { value: 'fr', label: '🇫🇷 FR' }, { value: 'es', label: '🇪🇸 ES' }, { value: 'de', label: '🇩🇪 DE' }, + { value: 'pt', label: '🇧🇷 PT' }, ]; const Option = (props: OptionProps) => ( @@ -23,16 +25,26 @@ const SingleVal = (props: SingleValueProps) => ( ); const LanguageSwitcher: React.FC = () => { - const [mounted, setMounted] = useState(false); - const [selected, setSelected] = useState(LANGS[0]); + const { i18n } = useTranslation(); + const [mounted, setMounted] = useState(false); + const [selected, setSelected] = useState(LANGS.find(l => l.value === i18n.language) || LANGS[0]); useEffect(() => { setMounted(true); }, []); + useEffect(() => { + const currentLang = LANGS.find(l => l.value === i18n.language); + if (currentLang) { + setSelected(currentLang); + } + }, [i18n.language]); + const handleChange = (opt: LanguageOption | null) => { if (!opt) return; setSelected(opt); + i18n.changeLanguage(opt.value); + localStorage.setItem('app_lang_', opt.value); }; if (!mounted) return null; @@ -93,4 +105,4 @@ const LanguageSwitcher: React.FC = () => { ); }; -export default LanguageSwitcher; +export default LanguageSwitcher; \ No newline at end of file diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 2bc1e8e..6fe767c 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -3,19 +3,66 @@ import { initReactI18next } from 'react-i18next'; import HttpApi from 'i18next-http-backend'; 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 localStorage.getItem('detected_country_lang'); + } + return undefined; + }, + cacheUserLanguage(lng: string) { + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.setItem('detected_country_lang', lng); + } + } +}; + +const languageDetector = new LanguageDetector(); +languageDetector.addDetector(countryLanguageDetector); + i18n .use(HttpApi) - .use(LanguageDetector) + .use(languageDetector) .use(initReactI18next) .init({ fallbackLng: 'en', + supportedLngs: ['en', 'fr', 'es', 'de', 'pt'], + ns: ['common'], + defaultNS: 'common', detection: { - order: ['localStorage', 'navigator'], + order: ['localStorage', 'cookie', 'countryLanguageDetector', 'navigator', 'htmlTag', 'path', 'subdomain'], lookupLocalStorage: 'app_lang_', - caches: ['localStorage'], + caches: ['localStorage', 'cookie'], }, backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', }, interpolation: { escapeValue: false }, - }); \ No newline at end of file + }); + +// Perform country detection asynchronously if not already detected +if (typeof window !== 'undefined' && window.localStorage && !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(','); + for (const lang of languages) { + const baseLang = lang.split('-')[0]; + if (['en', 'fr', 'es', 'de', 'pt'].includes(baseLang)) { + localStorage.setItem('detected_country_lang', baseLang); + if (!localStorage.getItem('app_lang_')) { + i18n.changeLanguage(baseLang); + } + break; + } + } + } + }) + .catch(err => console.error('Country language detection failed', err)); +} + +export default i18n; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index e886ab1..e9390a9 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -3,7 +3,7 @@ import Head from 'next/head'; import Link from 'next/link'; import LayoutGuest from '../layouts/Guest'; import SectionFullScreen from '../components/SectionFullScreen'; -import { mdiMusic, mdiMicrophone, mdiPiano, mdiChartTimelineVariant } from '@mdi/js'; +import { mdiMusic, mdiMicrophone, mdiPiano, mdiChartTimelineVariant, mdiShieldLock } from '@mdi/js'; import BaseIcon from '../components/BaseIcon'; import { getPageTitle } from '../config'; @@ -26,11 +26,9 @@ export default function Home() {
Features Instruments - - Login - - - Start Creating + + + Admin Access
@@ -44,20 +42,19 @@ export default function Home() {
- AI-Powered Music Composition + Owner's Private Workspace

- CREATE YOUR SYMPHONY IN SECONDS + ADVANCED PRODUCTION CONTROL

- From Sertanejo to Hip-Hop. Professional instruments, intelligent beats, and AI lyrics generator — all with just your access code. + Professional instruments, intelligent beats, and AI lyrics engine. + The ultimate music production environment, strictly for the studio owner.

- - Launch Studio - - - Enter with Code + + + Enter Admin Portal
@@ -86,21 +83,9 @@ export default function Home() { - {/* 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.

+

© 2026 {title}. Restricted Owner Access.

diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 658910c..3424b75 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -4,29 +4,26 @@ import Head from 'next/head'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import BaseIcon from "../components/BaseIcon"; -import { mdiInformation, mdiEye, mdiEyeOff, mdiKey, mdiEmail } from '@mdi/js'; +import { mdiKey, mdiShieldLock } from '@mdi/js'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; import { Field, Form, Formik } from 'formik'; import FormField from '../components/FormField'; -import FormCheckRadio from '../components/FormCheckRadio'; import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import { useRouter } from 'next/router'; import { getPageTitle } from '../config'; -import { findMe, loginUser, loginWithCode, resetAction } from '../stores/authSlice'; +import { findMe, loginAdmin, resetAction } from '../stores/authSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; -import Link from 'next/link'; import {toast, ToastContainer} from "react-toastify"; import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' +import { useTranslation } from 'react-i18next'; export default function Login() { + const { t } = useTranslation('common'); const router = useRouter(); 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 [loginMethod, setLoginMethod] = useState<'email' | 'code'>('code'); const [ illustrationImage, setIllustrationImage ] = useState({ src: undefined, photographer: undefined, @@ -35,13 +32,9 @@ export default function Login() { const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []}) const [contentType, setContentType] = useState('video'); const [contentPosition, setContentPosition] = useState('right'); - const [showPassword, setShowPassword] = useState(false); const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( (state) => state.auth, ); - const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com', - password: '8e470127', - remember: true }) const title = 'AI Music Studio' @@ -82,25 +75,8 @@ export default function Login() { } }, [notifyState?.showNotification]) - const togglePasswordVisibility = () => { - setShowPassword(!showPassword); - }; - - const handleSubmit = async (value) => { - if (loginMethod === 'email') { - const {remember, ...rest} = value - await dispatch(loginUser(rest)); - } else { - await dispatch(loginWithCode({ code: value.code })); - } - }; - - const setLogin = (target: HTMLElement) => { - setInitialValues(prev => ({ - ...prev, - email : target.innerText.trim(), - password: target.dataset.password ?? '', - })); + const handleSubmit = async (values) => { + await dispatch(loginAdmin({ key: values.key })); }; const imageBlock = (image) => ( @@ -112,8 +88,9 @@ export default function Login() { backgroundRepeat: 'no-repeat', }}>
- Photo - by {image?.photographer} on Pexels + + {t('pages.login.pexels.photoCredit', { photographer: image?.photographer, defaultValue: `Photo by ${image?.photographer} on Pexels` })} +
) @@ -129,7 +106,7 @@ export default function Login() { muted > - Your browser does not support the video tag. + {t('pages.login.pexels.videoUnsupported')}
- Video by {video.user.name} on Pexels + {t('pages.login.pexels.videoCredit', { name: video.user.name, defaultValue: `Video by ${video.user.name} on Pexels` })}
) @@ -157,7 +134,7 @@ export default function Login() { backgroundRepeat: 'no-repeat', } : {}}> - {getPageTitle('Login')} + {getPageTitle(t('pages.login.pageTitle'))} @@ -168,107 +145,47 @@ export default function Login() {

{title}

-

THE FUTURE OF SOUND

+

{t('pages.login.pageTitle')}

-
- - +
+
+ +
+

{t('pages.login.ownerAccess')}

+

{t('pages.login.enterPrivateKey')}

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

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

-
- )} +
+ + + +
- - - -

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

@@ -276,10 +193,7 @@ export default function Login() {
-

© 2026 {title}. Built for Creators.

- - Privacy Policy - +

© 2026 {title}. {t('pages.login.restrictedAccess')}

@@ -288,4 +202,4 @@ export default function Login() { 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 d245f78..0edbd6d 100644 --- a/frontend/src/pages/register.tsx +++ b/frontend/src/pages/register.tsx @@ -1,152 +1,57 @@ -import React, { useState } from 'react'; +import React, { useEffect } from 'react'; import type { ReactElement } from 'react'; -import { ToastContainer, toast } from 'react-toastify'; import Head from 'next/head'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; 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 { getPageTitle } from '../config'; +import CardBox from '../components/CardBox'; 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'; +import { mdiLockOff } from '@mdi/js'; +import BaseButton from '../components/BaseButton'; 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 router = useRouter(); - const handleSubmit = async (value) => { - setLoading(true) - try { - const { data: token } = await axios.post('/auth/signup', value); - const decoded: any = jwt.decode(token); - - 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) - } catch (error) { - setLoading(false) - console.log('error: ', error) - notify('error', 'Something went wrong. Try again') - } - }; + useEffect(() => { + // Redirect to login after 3 seconds + const timer = setTimeout(() => { + router.push('/login'); + }, 3000); + return () => clearTimeout(timer); + }, [router]); - const copyToClipboard = () => { - if (accessCode) { - navigator.clipboard.writeText(accessCode); - notify('info', 'Code copied to clipboard!'); - } - } + return ( + <> + + {getPageTitle('Registration Disabled')} + - return ( -
- - {getPageTitle('Register')} - - - -
-
-
- -

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 - - -
- )} + + +
+
+
- - -
- ); +

ACCESS RESTRICTED

+

+ This studio is strictly reserved for the owner. + Public registration has been disabled to ensure complete creative control. +

+ router.push('/login')} + /> +

Redirecting to portal...

+
+ +
+ + ); } Register.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; + return {page}; +}; \ No newline at end of file diff --git a/frontend/src/stores/authSlice.ts b/frontend/src/stores/authSlice.ts index 8fe1410..a3aebee 100644 --- a/frontend/src/stores/authSlice.ts +++ b/frontend/src/stores/authSlice.ts @@ -25,6 +25,21 @@ const initialState: MainState = { 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( 'auth/loginUser', async (creds: Record, { rejectWithValue }) => { @@ -121,6 +136,15 @@ export const authSlice = createSlice({ 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; });