2
This commit is contained in:
parent
14794ed687
commit
e1f7182cfc
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
frontend/public/locales/pt/common.json
Normal file
60
frontend/public/locales/pt/common.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"pageTitle": "Painel de Controle",
|
||||
"overview": "Visão Geral",
|
||||
"loadingWidgets": "Carregando widgets...",
|
||||
"loading": "Carregando..."
|
||||
},
|
||||
"login": {
|
||||
"pageTitle": "Acesso Administrativo",
|
||||
"ownerAccess": "Acesso do Proprietário",
|
||||
"enterPrivateKey": "Por favor, insira sua chave privada única",
|
||||
"privateKey": "Chave Privada",
|
||||
"privateKeyHelp": "Somente o proprietário da aplicação possui esta chave",
|
||||
"enterKeyPlaceholder": "Insira a chave...",
|
||||
"validating": "VALIDANDO...",
|
||||
"accessStudio": "ACESSAR ESTÚDIO",
|
||||
"restrictedAccess": "Acesso Restrito.",
|
||||
|
||||
"form": {
|
||||
"loginLabel": "Login",
|
||||
"loginHelp": "Por favor, insira seu login",
|
||||
"passwordLabel": "Senha",
|
||||
"passwordHelp": "Por favor, insira sua senha",
|
||||
"remember": "Lembrar",
|
||||
"forgotPassword": "Esqueceu a senha?",
|
||||
"loginButton": "Entrar",
|
||||
"loading": "Carregando...",
|
||||
"noAccountYet": "Ainda não tem uma conta?",
|
||||
"newAccount": "Nova Conta"
|
||||
},
|
||||
|
||||
"pexels": {
|
||||
"photoCredit": "Foto por {{photographer}} no Pexels",
|
||||
"videoCredit": "Vídeo por {{name}} no Pexels",
|
||||
"videoUnsupported": "Seu navegador não suporta a tag de vídeo."
|
||||
},
|
||||
|
||||
"footer": {
|
||||
"copyright": "© {{year}} {{title}}. Todos os direitos reservados",
|
||||
"privacy": "Política de Privacidade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"widgetCreator": {
|
||||
"title": "Criar Gráfico ou Widget",
|
||||
"helpText": "Descreva seu novo widget ou gráfico em linguagem natural. Por exemplo: \"Número de usuários admin\" OU \"gráfico vermelho com número de contratos fechados agrupados por mês"",
|
||||
"settingsTitle": "Configurações do Criador de Widget",
|
||||
"settingsDescription": "Para qual papel estamos mostrando e criando widgets?",
|
||||
"doneButton": "Concluído",
|
||||
"loading": "Carregando..."
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Pesquisar",
|
||||
"required": "Obrigatório",
|
||||
"minLength": "Comprimento mínimo: {{count}} caracteres"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<LanguageOption, false>) => (
|
||||
@ -23,16 +25,26 @@ const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
|
||||
);
|
||||
|
||||
const LanguageSwitcher: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [selected, setSelected] = useState<LanguageOption>(LANGS[0]);
|
||||
const { i18n } = useTranslation();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [selected, setSelected] = useState<LanguageOption>(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;
|
||||
@ -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 },
|
||||
});
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
@ -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() {
|
||||
<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 href="/login" className="px-6 py-2 rounded-full bg-[#00E5FF] text-black hover:bg-white transition-all shadow-[0_0_15px_rgba(0,229,255,0.4)] font-bold flex items-center space-x-2">
|
||||
<BaseIcon path={mdiShieldLock} size={18} />
|
||||
<span>Admin Access</span>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
@ -44,20 +42,19 @@ export default function Home() {
|
||||
|
||||
<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
|
||||
Owner's Private Workspace
|
||||
</div>
|
||||
<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
|
||||
ADVANCED <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#00E5FF] to-[#BB86FC]">PRODUCTION</span> CONTROL
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
|
||||
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.
|
||||
</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 href="/login" className="w-full sm:w-auto px-10 py-5 rounded-xl bg-[#00E5FF] text-black text-xl font-bold hover:scale-105 transition-transform shadow-[0_0_25px_rgba(0,229,255,0.5)] flex items-center justify-center space-x-3">
|
||||
<BaseIcon path={mdiShieldLock} size={24} />
|
||||
<span>Enter Admin Portal</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -86,21 +83,9 @@ export default function Home() {
|
||||
</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's Most Advanced AI Music Studio.</p>
|
||||
<p>© 2026 {title}. Restricted Owner Access.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -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',
|
||||
}}>
|
||||
<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>
|
||||
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">
|
||||
{t('pages.login.pexels.photoCredit', { photographer: image?.photographer, defaultValue: `Photo by ${image?.photographer} on Pexels` })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -129,7 +106,7 @@ export default function Login() {
|
||||
muted
|
||||
>
|
||||
<source src={video.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
{t('pages.login.pexels.videoUnsupported')}
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
@ -138,7 +115,7 @@ export default function Login() {
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
{t('pages.login.pexels.videoCredit', { name: video.user.name, defaultValue: `Video by ${video.user.name} on Pexels` })}
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
@ -157,7 +134,7 @@ export default function Login() {
|
||||
backgroundRepeat: 'no-repeat',
|
||||
} : {}}>
|
||||
<Head>
|
||||
<title>{getPageTitle('Login')}</title>
|
||||
<title>{getPageTitle(t('pages.login.pageTitle'))}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='dark'>
|
||||
@ -168,107 +145,47 @@ export default function Login() {
|
||||
|
||||
<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>
|
||||
<p className="text-[#00E5FF] font-bold uppercase tracking-widest">{t('pages.login.pageTitle')}</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 className="flex flex-col items-center mb-8">
|
||||
<div className="w-16 h-16 bg-gray-800 rounded-full flex items-center justify-center mb-4 border border-gray-700">
|
||||
<BaseIcon path={mdiShieldLock} size={32} className="text-[#00E5FF]" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">{t('pages.login.ownerAccess')}</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">{t('pages.login.enterPrivateKey')}</p>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
code: ''
|
||||
key: ''
|
||||
}}
|
||||
enableReinitialize
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
{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'} 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 className="mb-8">
|
||||
<FormField
|
||||
label={t('pages.login.privateKey')}
|
||||
help={t('pages.login.privateKeyHelp')}>
|
||||
<Field
|
||||
name='key'
|
||||
type="password"
|
||||
placeholder={t('pages.login.enterKeyPlaceholder')}
|
||||
className="bg-gray-800 border-gray-700 text-white text-xl text-center font-mono tracking-widest placeholder-gray-600 focus:ring-[#00E5FF] focus:border-[#00E5FF] rounded-xl py-6"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
className={'w-full py-4 text-xl font-black rounded-xl'}
|
||||
type='submit'
|
||||
label={isFetching ? 'ENTERING...' : 'ENTER STUDIO'}
|
||||
label={isFetching ? t('pages.login.validating') : t('pages.login.accessStudio')}
|
||||
color='info'
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</BaseButtons>
|
||||
|
||||
<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>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
@ -276,10 +193,7 @@ export default function Login() {
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<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>
|
||||
<p className='py-6 text-sm'>© 2026 <span className="text-white font-bold">{title}</span>. {t('pages.login.restrictedAccess')}</p>
|
||||
</div>
|
||||
<ToastContainer theme="dark" />
|
||||
</div>
|
||||
@ -288,4 +202,4 @@ export default function Login() {
|
||||
|
||||
Login.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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<string | null>(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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Registration Disabled')}</title>
|
||||
</Head>
|
||||
|
||||
return (
|
||||
<div className="bg-[#121212] min-h-screen">
|
||||
<Head>
|
||||
<title>{getPageTitle('Register')}</title>
|
||||
</Head>
|
||||
|
||||
<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>
|
||||
|
||||
{!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>
|
||||
|
||||
<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'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
|
||||
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>
|
||||
</CardBox>
|
||||
)}
|
||||
<SectionFullScreen bg='dark'>
|
||||
<CardBox className="w-full md:w-1/2 lg:w-1/3 bg-gray-900 border-gray-800 text-center py-12 px-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-20 h-20 bg-red-900/20 rounded-full flex items-center justify-center mb-6 border border-red-900/50">
|
||||
<BaseIcon path={mdiLockOff} size={40} className="text-red-500" />
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<ToastContainer theme="dark" />
|
||||
</div>
|
||||
);
|
||||
<h1 className="text-3xl font-black text-white mb-4">ACCESS RESTRICTED</h1>
|
||||
<p className="text-gray-400 mb-8 leading-relaxed">
|
||||
This studio is strictly reserved for the owner.
|
||||
Public registration has been disabled to ensure complete creative control.
|
||||
</p>
|
||||
<BaseButton
|
||||
label="Back to Admin Access"
|
||||
color="info"
|
||||
className="w-full py-4 font-bold"
|
||||
onClick={() => router.push('/login')}
|
||||
/>
|
||||
<p className="text-xs text-gray-600 mt-6 italic">Redirecting to portal...</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Register.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
@ -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<string, string>, { 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;
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user