Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@ -16,59 +16,91 @@ module.exports = function(sequelize, DataTypes) {
|
|||||||
|
|
||||||
project_name: {
|
project_name: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
project_status: {
|
project_status: {
|
||||||
type: DataTypes.ENUM,
|
type: DataTypes.ENUM,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
values: [
|
values: [
|
||||||
"idea",
|
|
||||||
"generating",
|
"idea",
|
||||||
"building",
|
|
||||||
"testing",
|
|
||||||
"ready",
|
"generating",
|
||||||
"failed"
|
|
||||||
|
|
||||||
|
"building",
|
||||||
|
|
||||||
|
|
||||||
|
"testing",
|
||||||
|
|
||||||
|
|
||||||
|
"ready",
|
||||||
|
|
||||||
|
|
||||||
|
"failed"
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
target_dimension: {
|
target_dimension: {
|
||||||
type: DataTypes.ENUM,
|
type: DataTypes.ENUM,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
values: [
|
values: [
|
||||||
"2d",
|
|
||||||
"3d",
|
"2d",
|
||||||
"mixed"
|
|
||||||
|
|
||||||
|
"3d",
|
||||||
|
|
||||||
|
|
||||||
|
"mixed"
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
game_concept: {
|
game_concept: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
design_document: {
|
design_document: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
configuration_notes: {
|
configuration_notes: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
requested_at: {
|
requested_at: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
completed_at: {
|
completed_at: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
play_url: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
download_url_pc: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
download_url_mobile: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
@ -85,6 +117,30 @@ download_url_mobile: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ai_game_projects.associate = (db) => {
|
ai_game_projects.associate = (db) => {
|
||||||
|
|
||||||
|
|
||||||
|
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//end loop
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.ai_game_projects.belongsTo(db.users, {
|
db.ai_game_projects.belongsTo(db.users, {
|
||||||
as: 'owner_user',
|
as: 'owner_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -101,6 +157,8 @@ download_url_mobile: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.ai_game_projects.hasMany(db.file, {
|
db.ai_game_projects.hasMany(db.file, {
|
||||||
as: 'project_files',
|
as: 'project_files',
|
||||||
foreignKey: 'belongsToId',
|
foreignKey: 'belongsToId',
|
||||||
@ -111,6 +169,7 @@ download_url_mobile: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.ai_game_projects.belongsTo(db.users, {
|
db.ai_game_projects.belongsTo(db.users, {
|
||||||
as: 'createdBy',
|
as: 'createdBy',
|
||||||
});
|
});
|
||||||
@ -120,5 +179,9 @@ download_url_mobile: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return ai_game_projects;
|
return ai_game_projects;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -61,10 +61,6 @@ revoked_reason: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
guest_id: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -142,4 +138,6 @@ guest_id: {
|
|||||||
|
|
||||||
|
|
||||||
return game_access_passes;
|
return game_access_passes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -84,10 +84,6 @@ expires_at: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
guest_id: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -181,4 +177,6 @@ guest_id: {
|
|||||||
|
|
||||||
|
|
||||||
return orders;
|
return orders;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -114,19 +115,22 @@ app.use('/api/permissions', passport.authenticate('jwt', {session: false}), perm
|
|||||||
|
|
||||||
app.use('/api/admin_keys', passport.authenticate('jwt', {session: false}), admin_keysRoutes);
|
app.use('/api/admin_keys', passport.authenticate('jwt', {session: false}), admin_keysRoutes);
|
||||||
|
|
||||||
// Public access for game-related entities
|
app.use('/api/game_categories', passport.authenticate('jwt', {session: false}), game_categoriesRoutes);
|
||||||
app.use('/api/game_categories', game_categoriesRoutes);
|
|
||||||
app.use('/api/games', gamesRoutes);
|
app.use('/api/games', passport.authenticate('jwt', {session: false}), gamesRoutes);
|
||||||
app.use('/api/game_time_passes', game_time_passesRoutes);
|
|
||||||
app.use('/api/game_payment_qr_codes', game_payment_qr_codesRoutes);
|
app.use('/api/game_time_passes', passport.authenticate('jwt', {session: false}), game_time_passesRoutes);
|
||||||
app.use('/api/ai_game_projects', ai_game_projectsRoutes);
|
|
||||||
|
|
||||||
app.use('/api/payment_providers', passport.authenticate('jwt', {session: false}), payment_providersRoutes);
|
app.use('/api/payment_providers', passport.authenticate('jwt', {session: false}), payment_providersRoutes);
|
||||||
|
|
||||||
|
app.use('/api/game_payment_qr_codes', passport.authenticate('jwt', {session: false}), game_payment_qr_codesRoutes);
|
||||||
|
|
||||||
app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes);
|
app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes);
|
||||||
|
|
||||||
app.use('/api/game_access_passes', passport.authenticate('jwt', {session: false}), game_access_passesRoutes);
|
app.use('/api/game_access_passes', passport.authenticate('jwt', {session: false}), game_access_passesRoutes);
|
||||||
|
|
||||||
|
app.use('/api/ai_game_projects', passport.authenticate('jwt', {session: false}), ai_game_projectsRoutes);
|
||||||
|
|
||||||
app.use('/api/sms_verification_codes', passport.authenticate('jwt', {session: false}), sms_verification_codesRoutes);
|
app.use('/api/sms_verification_codes', passport.authenticate('jwt', {session: false}), sms_verification_codesRoutes);
|
||||||
|
|
||||||
app.use('/api/localization_events', passport.authenticate('jwt', {session: false}), localization_eventsRoutes);
|
app.use('/api/localization_events', passport.authenticate('jwt', {session: false}), localization_eventsRoutes);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const Ai_game_projectsService = require('../services/ai_game_projects');
|
const Ai_game_projectsService = require('../services/ai_game_projects');
|
||||||
@ -92,39 +93,6 @@ router.post('/', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /api/ai_game_projects/generate:
|
|
||||||
* post:
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags: [Ai_game_projects]
|
|
||||||
* summary: Generate AI game project
|
|
||||||
* description: Generate AI game project from concept
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* properties:
|
|
||||||
* data:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* project_name:
|
|
||||||
* type: string
|
|
||||||
* game_concept:
|
|
||||||
* type: string
|
|
||||||
* target_dimension:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: The project generation started
|
|
||||||
*/
|
|
||||||
router.post('/generate', wrapAsync(async (req, res) => {
|
|
||||||
const payload = await Ai_game_projectsService.generate(req.body.data, req.currentUser);
|
|
||||||
res.status(200).send(payload);
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/budgets/bulk-import:
|
* /api/budgets/bulk-import:
|
||||||
@ -469,4 +437,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -4,10 +4,31 @@ const passport = require('passport');
|
|||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const AuthService = require('../services/auth');
|
const AuthService = require('../services/auth');
|
||||||
const ForbiddenError = require('../services/notifications/errors/forbidden');
|
const ForbiddenError = require('../services/notifications/errors/forbidden');
|
||||||
|
const EmailSender = require('../services/email');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Auth:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* default: admin@flatlogic.com
|
||||||
|
* description: User email
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* default: password
|
||||||
|
* description: User password
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* tags:
|
* tags:
|
||||||
@ -17,59 +38,27 @@ const router = express.Router();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/auth/signin/private-key:
|
* /api/auth/signin/local:
|
||||||
* post:
|
* post:
|
||||||
* tags: [Auth]
|
* tags: [Auth]
|
||||||
* summary: Logs admin using a private key
|
* summary: Logs user into the system
|
||||||
* description: Logs admin using a private key
|
* description: Logs user into the system
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* description: Set valid private key
|
* description: Set valid user email and password
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* $ref: "#/components/schemas/Auth"
|
||||||
* required:
|
|
||||||
* - privateKey
|
|
||||||
* properties:
|
|
||||||
* privateKey:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Successful login
|
* description: Successful login
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid private key
|
* description: Invalid username/password supplied
|
||||||
|
* x-codegen-request-body-name: body
|
||||||
*/
|
*/
|
||||||
router.post('/signin/private-key', wrapAsync(async (req, res) => {
|
|
||||||
const payload = await AuthService.signinWithPrivateKey(req.body.privateKey, req);
|
|
||||||
res.status(200).send(payload);
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
router.post('/signin/local', wrapAsync(async (req, res) => {
|
||||||
* @swagger
|
const payload = await AuthService.signin(req.body.email, req.body.password, req,);
|
||||||
* /api/auth/signin/access-code:
|
|
||||||
* post:
|
|
||||||
* tags: [Auth]
|
|
||||||
* summary: Logs user using a 6-digit access code
|
|
||||||
* description: Logs user using a 6-digit 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 access code
|
|
||||||
*/
|
|
||||||
router.post('/signin/access-code', wrapAsync(async (req, res) => {
|
|
||||||
const payload = await AuthService.signinWithAccessCode(req.body.code, req);
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -86,8 +75,10 @@ router.post('/signin/access-code', wrapAsync(async (req, res) => {
|
|||||||
* 200:
|
* 200:
|
||||||
* description: Successful retrieval of current authorized user data
|
* description: Successful retrieval of current authorized user data
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid token supplied
|
* description: Invalid username/password supplied
|
||||||
|
* x-codegen-request-body-name: body
|
||||||
*/
|
*/
|
||||||
|
|
||||||
router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => {
|
router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => {
|
||||||
if (!req.currentUser || !req.currentUser.id) {
|
if (!req.currentUser || !req.currentUser.id) {
|
||||||
throw new ForbiddenError();
|
throw new ForbiddenError();
|
||||||
@ -98,6 +89,68 @@ router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) =>
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.put('/password-reset', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
|
||||||
|
const payload = await AuthService.passwordUpdate(req.body.currentPassword, req.body.newPassword, req);
|
||||||
|
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-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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
|
router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
|
||||||
if (!req.currentUser || !req.currentUser.id) {
|
if (!req.currentUser || !req.currentUser.id) {
|
||||||
throw new ForbiddenError();
|
throw new ForbiddenError();
|
||||||
@ -108,6 +161,47 @@ router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
router.put('/verify-email', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer)
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/email-configured', (req, res) => {
|
||||||
|
const payload = EmailSender.isConfigured;
|
||||||
|
res.status(200).send(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/signin/google', (req, res, next) => {
|
||||||
|
passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}),
|
||||||
|
|
||||||
|
function (req, res) {
|
||||||
|
socialRedirect(res, req.query.state, req.user.token, config);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/signin/microsoft', (req, res, next) => {
|
||||||
|
passport.authenticate("microsoft", {
|
||||||
|
scope: ["https://graph.microsoft.com/user.read openid"],
|
||||||
|
state: req.query.app
|
||||||
|
})(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/signin/microsoft/callback', passport.authenticate("microsoft", {
|
||||||
|
failureRedirect: "/login",
|
||||||
|
session: false
|
||||||
|
}),
|
||||||
|
function (req, res) {
|
||||||
|
socialRedirect(res, req.query.state, req.user.token, config);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
|
function socialRedirect(res, state, token, config) {
|
||||||
|
res.redirect(config.uiUrl + "/login?token=" + token);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const GamesService = require('../services/games');
|
const GamesService = require('../services/games');
|
||||||
@ -9,18 +10,6 @@ const router = express.Router();
|
|||||||
|
|
||||||
const { parse } = require('json2csv');
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
// Public routes for purchase and verification
|
|
||||||
router.post('/purchase', wrapAsync(async (req, res) => {
|
|
||||||
const { gameId, timePassId, guestId } = req.body;
|
|
||||||
const payload = await GamesService.purchase(gameId, timePassId, guestId, req.currentUser);
|
|
||||||
res.status(200).send(payload);
|
|
||||||
}));
|
|
||||||
|
|
||||||
router.get('/verify-access', wrapAsync(async (req, res) => {
|
|
||||||
const { gameId, guestId } = req.query;
|
|
||||||
const payload = await GamesService.verifyAccess(gameId, guestId, req.currentUser);
|
|
||||||
res.status(200).send(payload);
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
@ -470,4 +459,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -6,7 +6,10 @@ const csv = require('csv-parser');
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const { LocalAIApi } = require('../ai/LocalAIApi');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Ai_game_projectsService {
|
module.exports = class Ai_game_projectsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
@ -27,116 +30,34 @@ module.exports = class Ai_game_projectsService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static async generate(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
// 1. Create the project record with "generating" status
|
|
||||||
const projectData = {
|
|
||||||
...data,
|
|
||||||
project_status: 'generating',
|
|
||||||
requested_at: new Date(),
|
|
||||||
owner_userId: currentUser ? currentUser.id : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createdProject = await Ai_game_projectsDBApi.create(
|
|
||||||
projectData,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
|
|
||||||
// 2. Trigger AI generation
|
|
||||||
const prompt = `You are an expert game developer. Create a comprehensive Game Design Document (GDD) and technical architecture for a fully functional ${data.target_dimension || '2D'} game.
|
|
||||||
Concept: "${data.game_concept}".
|
|
||||||
|
|
||||||
Include:
|
|
||||||
1. Game Mechanics (Fully defined)
|
|
||||||
2. Level Structure
|
|
||||||
3. Technical Build Specs (PCs, Smartphones, Tablets)
|
|
||||||
4. Asset Manifest
|
|
||||||
5. Logic Flow (Input, Physics, UI)
|
|
||||||
|
|
||||||
The game must be ready for automatic compilation and deployment across all platforms.`;
|
|
||||||
|
|
||||||
const aiResponse = await LocalAIApi.createResponse({
|
|
||||||
input: [
|
|
||||||
{ role: 'system', content: 'You are an advanced autonomous game development engine.' },
|
|
||||||
{ role: 'user', content: prompt }
|
|
||||||
],
|
|
||||||
options: { poll_interval: 5, poll_timeout: 300 }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (aiResponse.success) {
|
|
||||||
const designDoc = LocalAIApi.extractText(aiResponse);
|
|
||||||
|
|
||||||
// Simulate "Building" phase
|
|
||||||
await Ai_game_projectsDBApi.update(
|
|
||||||
createdProject.id,
|
|
||||||
{ project_status: 'building' },
|
|
||||||
{ currentUser }
|
|
||||||
);
|
|
||||||
|
|
||||||
// In a real scenario, this is where compilation happens.
|
|
||||||
// For the prototype, we provide the functional endpoints immediately after "build"
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000)); // Simulate building time
|
|
||||||
|
|
||||||
// 3. Update the record with generated content and download links
|
|
||||||
await Ai_game_projectsDBApi.update(
|
|
||||||
createdProject.id,
|
|
||||||
{
|
|
||||||
design_document: designDoc,
|
|
||||||
project_status: 'ready',
|
|
||||||
completed_at: new Date(),
|
|
||||||
// Functional game links (Simulated)
|
|
||||||
play_url: `https://nexus-games-runtime.io/play/${createdProject.id}`,
|
|
||||||
download_url_pc: `https://nexus-games-cdn.io/builds/pc/${createdProject.id}.exe`,
|
|
||||||
download_url_mobile: `https://nexus-games-cdn.io/builds/mobile/${createdProject.id}.apk`,
|
|
||||||
},
|
|
||||||
{ currentUser }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await Ai_game_projectsDBApi.update(
|
|
||||||
createdProject.id,
|
|
||||||
{
|
|
||||||
project_status: 'failed',
|
|
||||||
configuration_notes: aiResponse.error || 'AI Generation failed',
|
|
||||||
},
|
|
||||||
{ currentUser }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createdProject;
|
|
||||||
} catch (error) {
|
|
||||||
if (transaction) await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await processFile(req, res);
|
await processFile(req, res);
|
||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
const results = [];
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
|
||||||
|
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (data) => results.push(data))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
|
console.log('CSV results', results);
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
})
|
})
|
||||||
|
|
||||||
await Ai_game_projectsDBApi.bulkImport(results, {
|
await Ai_game_projectsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: true,
|
validate: true,
|
||||||
currentUser: req.currentUser
|
currentUser: req.currentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@ -147,11 +68,29 @@ module.exports = class Ai_game_projectsService {
|
|||||||
static async update(data, id, currentUser) {
|
static async update(data, id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
let ai_game_projects = await Ai_game_projectsDBApi.findBy({id}, {transaction});
|
let ai_game_projects = await Ai_game_projectsDBApi.findBy(
|
||||||
if (!ai_game_projects) throw new ValidationError('ai_game_projectsNotFound');
|
{id},
|
||||||
const updatedAi_game_projects = await Ai_game_projectsDBApi.update(id, data, { currentUser, transaction });
|
{transaction},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ai_game_projects) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'ai_game_projectsNotFound',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAi_game_projects = await Ai_game_projectsDBApi.update(
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return updatedAi_game_projects;
|
return updatedAi_game_projects;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
@ -160,8 +99,13 @@ module.exports = class Ai_game_projectsService {
|
|||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Ai_game_projectsDBApi.deleteByIds(ids, { currentUser, transaction });
|
await Ai_game_projectsDBApi.deleteByIds(ids, {
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@ -171,12 +115,24 @@ module.exports = class Ai_game_projectsService {
|
|||||||
|
|
||||||
static async remove(id, currentUser) {
|
static async remove(id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Ai_game_projectsDBApi.remove(id, { currentUser, transaction });
|
await Ai_game_projectsDBApi.remove(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,99 +2,305 @@ const UsersDBApi = require('../db/api/users');
|
|||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
const EmailAddressVerificationEmail = require('./email/list/addressVerification');
|
||||||
|
const InvitationEmail = require("./email/list/invitation");
|
||||||
|
const PasswordResetEmail = require('./email/list/passwordReset');
|
||||||
|
const EmailSender = require('./email');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const helpers = require('../helpers');
|
const helpers = require('../helpers');
|
||||||
const db = require('../db/models');
|
|
||||||
|
|
||||||
class Auth {
|
class Auth {
|
||||||
static async signup(email, password, options = {}, host) {
|
static async signup(email, password, options = {}, host) {
|
||||||
// Disabled as per user request to remove account creation options
|
const user = await UsersDBApi.findBy({email});
|
||||||
throw new ValidationError('auth.signupDisabled');
|
|
||||||
|
const hashedPassword = await bcrypt.hash(
|
||||||
|
password,
|
||||||
|
config.bcrypt.saltRounds,
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (EmailSender.isConfigured) {
|
||||||
|
await this.sendEmailAddressVerificationEmail(
|
||||||
|
user.email,
|
||||||
|
host,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return helpers.jwtSign(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = await UsersDBApi.createFromAuth(
|
||||||
|
{
|
||||||
|
firstName: email.split('@')[0],
|
||||||
|
password: hashedPassword,
|
||||||
|
email: email,
|
||||||
|
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (EmailSender.isConfigured) {
|
||||||
|
await this.sendEmailAddressVerificationEmail(
|
||||||
|
newUser.email,
|
||||||
|
host,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
id: newUser.id,
|
||||||
|
email: newUser.email
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return helpers.jwtSign(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async signin(email, password, options = {}) {
|
static async signin(email, password, options = {}) {
|
||||||
// Disabled as per user request to remove email/password login
|
const user = await UsersDBApi.findBy({email});
|
||||||
throw new ValidationError('auth.signinDisabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
static async signinWithPrivateKey(privateKey, options = {}) {
|
|
||||||
// Hardcoded unique admin private key from user request
|
|
||||||
const ADMIN_PRIVATE_KEY = '53e293e552b94270a64cb4d42811dabb4c6bd6726c3c4b42adb21a167b5e4d83';
|
|
||||||
|
|
||||||
if (privateKey !== ADMIN_PRIVATE_KEY) {
|
|
||||||
throw new ValidationError('auth.invalidPrivateKey');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the admin user
|
|
||||||
const user = await UsersDBApi.findBy({ email: 'admin@flatlogic.com' });
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ValidationError('auth.adminUserNotFound');
|
throw new ValidationError(
|
||||||
|
'auth.userNotFound',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.disabled) {
|
if (user.disabled) {
|
||||||
throw new ValidationError('auth.userDisabled');
|
throw new ValidationError(
|
||||||
|
'auth.userDisabled',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.password) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.wrongPassword',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EmailSender.isConfigured) {
|
||||||
|
user.emailVerified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.emailVerified) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.userNotVerified',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordsMatch = await bcrypt.compare(
|
||||||
|
password,
|
||||||
|
user.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!passwordsMatch) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.wrongPassword',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email
|
||||||
role: 'admin' // Explicitly marking as admin
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return helpers.jwtSign(data);
|
return helpers.jwtSign(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async signinWithAccessCode(code, options = {}) {
|
static async sendEmailAddressVerificationEmail(
|
||||||
// Users use a 6-digit code to access the platform
|
email,
|
||||||
if (!code || code.length !== 6 || !/^\d+$/.test(code)) {
|
host,
|
||||||
throw new ValidationError('auth.invalidAccessCode');
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
let link;
|
||||||
|
try {
|
||||||
|
const token = await UsersDBApi.generateEmailVerificationToken(
|
||||||
|
email,
|
||||||
|
);
|
||||||
|
link = `${host}/verify-email?token=${token}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.emailAddressVerificationEmail.error',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For common users, we don't necessarily need a persistent user record
|
const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
|
||||||
// in the same way, but we can return a JWT that identifies them by their code/session
|
email,
|
||||||
const data = {
|
link,
|
||||||
user: {
|
);
|
||||||
id: `guest_${code}`,
|
|
||||||
email: `guest_${code}@platform.com`,
|
|
||||||
role: 'user',
|
|
||||||
guestId: code
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return helpers.jwtSign(data);
|
return new EmailSender(
|
||||||
|
emailAddressVerificationEmail,
|
||||||
|
).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async sendEmailAddressVerificationEmail() {
|
static async sendPasswordResetEmail(email, type = 'register', host) {
|
||||||
throw new ValidationError('auth.featureDisabled');
|
|
||||||
|
|
||||||
|
let link;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await UsersDBApi.generatePasswordResetToken(
|
||||||
|
email,
|
||||||
|
);
|
||||||
|
link = `${host}/password-reset?token=${token}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.passwordReset.error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let passwordResetEmail;
|
||||||
|
if (type === 'register') {
|
||||||
|
passwordResetEmail = new PasswordResetEmail(
|
||||||
|
email,
|
||||||
|
link,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'invitation') {
|
||||||
|
passwordResetEmail = new InvitationEmail(
|
||||||
|
email,
|
||||||
|
link,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmailSender(passwordResetEmail).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async sendPasswordResetEmail() {
|
static async verifyEmail(token, options = {}) {
|
||||||
throw new ValidationError('auth.featureDisabled');
|
const user = await UsersDBApi.findByEmailVerificationToken(
|
||||||
|
token,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.emailAddressVerificationEmail.invalidToken',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return UsersDBApi.markEmailVerified(
|
||||||
|
user.id,
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async verifyEmail() {
|
static async passwordUpdate(currentPassword, newPassword, options) {
|
||||||
throw new ValidationError('auth.featureDisabled');
|
const currentUser = options.currentUser || null;
|
||||||
|
if (!currentUser) {
|
||||||
|
throw new ForbiddenError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPasswordMatch = await bcrypt.compare(
|
||||||
|
currentPassword,
|
||||||
|
currentUser.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentPasswordMatch) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'auth.wrongPassword'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
return UsersDBApi.updatePassword(
|
||||||
|
currentUser.id,
|
||||||
|
hashedPassword,
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async passwordUpdate() {
|
static async passwordReset(
|
||||||
throw new ValidationError('auth.featureDisabled');
|
token,
|
||||||
}
|
password,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const user = await UsersDBApi.findByPasswordResetToken(
|
||||||
|
token,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
static async passwordReset() {
|
if (!user) {
|
||||||
throw new ValidationError('auth.featureDisabled');
|
throw new ValidationError(
|
||||||
|
'auth.passwordReset.invalidToken',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(
|
||||||
|
password,
|
||||||
|
config.bcrypt.saltRounds,
|
||||||
|
);
|
||||||
|
|
||||||
|
return UsersDBApi.updatePassword(
|
||||||
|
user.id,
|
||||||
|
hashedPassword,
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateProfile(data, currentUser) {
|
static async updateProfile(data, currentUser) {
|
||||||
if (currentUser.role !== 'admin') {
|
|
||||||
throw new ForbiddenError();
|
|
||||||
}
|
|
||||||
// Only admin can update profile if needed
|
|
||||||
let transaction = await db.sequelize.transaction();
|
let transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await UsersDBApi.update(currentUser.id, data, { currentUser, transaction });
|
await UsersDBApi.findBy(
|
||||||
|
{id: currentUser.id},
|
||||||
|
{transaction},
|
||||||
|
);
|
||||||
|
|
||||||
|
await UsersDBApi.update(
|
||||||
|
currentUser.id,
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
currentUser,
|
||||||
|
transaction
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
|
|||||||
@ -6,7 +6,10 @@ const csv = require('csv-parser');
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const { Op } = require('sequelize');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class GamesService {
|
module.exports = class GamesService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
@ -129,77 +132,7 @@ module.exports = class GamesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async purchase(gameId, timePassId, guestId, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
};
|
||||||
try {
|
|
||||||
const game = await db.games.findByPk(gameId, { transaction });
|
|
||||||
const timePass = await db.game_time_passes.findByPk(timePassId, { transaction });
|
|
||||||
|
|
||||||
if (!game || !timePass) {
|
|
||||||
throw new ValidationError('Game or Time Pass not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a simulated order
|
|
||||||
const order = await db.orders.create({
|
|
||||||
status: 'paid',
|
|
||||||
amount: timePass.price,
|
|
||||||
currency: 'USD',
|
|
||||||
userId: currentUser ? currentUser.id : null,
|
|
||||||
guest_id: guestId,
|
|
||||||
time_passId: timePassId,
|
|
||||||
gameId: gameId,
|
|
||||||
paid_at: new Date(),
|
|
||||||
}, { transaction });
|
|
||||||
|
|
||||||
// Calculate access duration
|
|
||||||
const starts_at = new Date();
|
|
||||||
let ends_at = new Date(starts_at);
|
|
||||||
|
|
||||||
if (timePass.duration_days) {
|
|
||||||
ends_at.setDate(ends_at.getDate() + timePass.duration_days);
|
|
||||||
} else {
|
|
||||||
// Default to 1 day if not specified or fallback
|
|
||||||
ends_at.setDate(ends_at.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessPass = await db.game_access_passes.create({
|
|
||||||
starts_at,
|
|
||||||
ends_at,
|
|
||||||
status: 'active',
|
|
||||||
userId: currentUser ? currentUser.id : null,
|
|
||||||
guest_id: guestId,
|
|
||||||
gameId: gameId,
|
|
||||||
orderId: order.id,
|
|
||||||
}, { transaction });
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
accessPass,
|
|
||||||
playUrl: game.web_play_url
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async verifyAccess(gameId, guestId, currentUser) {
|
|
||||||
const where = {
|
|
||||||
gameId: gameId,
|
|
||||||
status: 'active',
|
|
||||||
ends_at: {
|
|
||||||
[Op.gt]: new Date()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentUser) {
|
|
||||||
where.userId = currentUser.id;
|
|
||||||
} else {
|
|
||||||
where.guest_id = guestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const access = await db.game_access_passes.findOne({ where });
|
|
||||||
return !!access;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@ -2,7 +2,6 @@ import React from 'react'
|
|||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import AsideMenuLayer from './AsideMenuLayer'
|
import AsideMenuLayer from './AsideMenuLayer'
|
||||||
import OverlayLayer from './OverlayLayer'
|
import OverlayLayer from './OverlayLayer'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuAsideItem[]
|
menu: MenuAsideItem[]
|
||||||
@ -16,24 +15,10 @@ export default function AsideMenu({
|
|||||||
isAsideLgActive = false,
|
isAsideLgActive = false,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
|
|
||||||
// Filter menu items based on admin role
|
|
||||||
const isAdmin = currentUser?.email === 'admin@flatlogic.com';
|
|
||||||
|
|
||||||
const filteredMenu = props.menu.filter(item => {
|
|
||||||
// Basic dashboard is for everyone
|
|
||||||
if (item.href === '/dashboard') return true;
|
|
||||||
if (item.href === '/profile') return true;
|
|
||||||
|
|
||||||
// Everything else requires admin role
|
|
||||||
return isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AsideMenuLayer
|
<AsideMenuLayer
|
||||||
menu={filteredMenu}
|
menu={props.menu}
|
||||||
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
|
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
|
||||||
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
||||||
}`}
|
}`}
|
||||||
@ -42,4 +27,4 @@ export default function AsideMenu({
|
|||||||
{isAsideLgActive && <OverlayLayer zIndex="z-30" onClick={props.onAsideLgClose} />}
|
{isAsideLgActive && <OverlayLayer zIndex="z-30" onClick={props.onAsideLgClose} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,84 +1,100 @@
|
|||||||
import React, { ReactNode, useState, useEffect } from 'react'
|
import React, {useEffect, useRef} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useState } from 'react'
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import UserAvatarCurrentUser from './UserAvatarCurrentUser'
|
import UserAvatarCurrentUser from './UserAvatarCurrentUser'
|
||||||
import NavBarMenuList from './NavBarMenuList'
|
import NavBarMenuList from './NavBarMenuList'
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import { MenuNavBarItem } from '../interfaces'
|
import { MenuNavBarItem } from '../interfaces'
|
||||||
|
import { setDarkMode } from '../stores/styleSlice'
|
||||||
import { logoutUser } from '../stores/authSlice'
|
import { logoutUser } from '../stores/authSlice'
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import ClickOutside from "./ClickOutside";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: MenuNavBarItem
|
item: MenuNavBarItem
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBarItem({ item }: Props) {
|
export default function NavBarItem({ item }: Props) {
|
||||||
const dispatch = useAppDispatch()
|
const router = useRouter();
|
||||||
const router = useRouter()
|
const dispatch = useAppDispatch();
|
||||||
const { currentUser } = useAppSelector((state) => state.auth)
|
const excludedRef = useRef(null);
|
||||||
|
|
||||||
|
const navBarItemLabelActiveColorStyle = useAppSelector(
|
||||||
|
(state) => state.style.navBarItemLabelActiveColorStyle
|
||||||
|
)
|
||||||
|
const navBarItemLabelStyle = useAppSelector((state) => state.style.navBarItemLabelStyle)
|
||||||
|
const navBarItemLabelHoverStyle = useAppSelector((state) => state.style.navBarItemLabelHoverStyle)
|
||||||
|
|
||||||
|
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||||
|
|
||||||
|
const userName = `${currentUser?.firstName ? currentUser?.firstName : ""} ${currentUser?.lastName ? currentUser?.lastName : ""}`;
|
||||||
|
|
||||||
const [isDropdownActive, setIsDropdownActive] = useState(false)
|
const [isDropdownActive, setIsDropdownActive] = useState(false)
|
||||||
|
|
||||||
const activeClassAddon =
|
useEffect(() => {
|
||||||
item.href && router.asPath === item.href ? 'text-blue-600 dark:text-slate-400' : ''
|
return () => setIsDropdownActive(false);
|
||||||
|
}, [router.pathname]);
|
||||||
|
|
||||||
const wrapperClass = `block lg:flex items-center relative cursor-pointer ${
|
const componentClass = [
|
||||||
item.menu ? 'bg-gray-100 lg:bg-transparent dark:bg-slate-800 lg:dark:bg-transparent' : ''
|
'block lg:flex items-center relative cursor-pointer',
|
||||||
}`
|
isDropdownActive
|
||||||
|
? `${navBarItemLabelActiveColorStyle} dark:text-slate-400`
|
||||||
|
: `${navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${navBarItemLabelHoverStyle}`,
|
||||||
|
item.menu ? 'lg:py-2 lg:px-3' : 'py-2 px-3',
|
||||||
|
item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '',
|
||||||
|
].join(' ')
|
||||||
|
|
||||||
const baseClass = `flex items-center p-3 lg:bg-transparent transition-colors duration-300 hover:text-blue-600 dark:hover:text-slate-400 ${activeClassAddon}`
|
const itemLabel = item.isCurrentUser ? userName : item.label
|
||||||
|
|
||||||
const getLabel = () => {
|
|
||||||
if (item.isCurrentUser) {
|
|
||||||
if (currentUser) {
|
|
||||||
return currentUser.firstName || currentUser.email
|
|
||||||
}
|
|
||||||
return 'Guest'
|
|
||||||
}
|
|
||||||
return item.label
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMenuClick = () => {
|
const handleMenuClick = () => {
|
||||||
if (item.menu) {
|
if (item.menu) {
|
||||||
setIsDropdownActive(!isDropdownActive)
|
setIsDropdownActive(!isDropdownActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.isLogout) {
|
if (item.isToggleLightDark) {
|
||||||
|
dispatch(setDarkMode(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
if(item.isLogout) {
|
||||||
dispatch(logoutUser())
|
dispatch(logoutUser())
|
||||||
router.push('/admin-login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const getItemId = (label) => {
|
||||||
const handleRouteChange = () => {
|
switch (label) {
|
||||||
setIsDropdownActive(false)
|
case 'Light/Dark':
|
||||||
|
return 'themeToggle';
|
||||||
|
case 'Log out':
|
||||||
|
return 'logout';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
router.events.on('routeChangeStart', handleRouteChange)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
router.events.off('routeChangeStart', handleRouteChange)
|
|
||||||
}
|
|
||||||
}, [router.events])
|
|
||||||
|
|
||||||
const NavBarItemComponentContents = (
|
const NavBarItemComponentContents = (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`${baseClass} ${
|
id={getItemId(itemLabel)}
|
||||||
isDropdownActive ? 'lg:bg-gray-100 lg:dark:bg-slate-800' : ''
|
className={`flex items-center ${
|
||||||
|
item.menu
|
||||||
|
? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0'
|
||||||
|
: 'w-full'
|
||||||
}`}
|
}`}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
>
|
>
|
||||||
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
|
{item.icon && <BaseIcon path={item.icon} size={22} className="transition-colors" />}
|
||||||
{item.icon && <BaseIcon path={item.icon} className="transition-colors" />}
|
|
||||||
<span
|
<span
|
||||||
className={`px-2 transition-colors ${
|
className={`px-2 transition-colors w-40 grow ${
|
||||||
item.menu ? 'lg:hidden' : ''
|
item.isDesktopNoLabel && item.icon ? 'lg:hidden' : ''
|
||||||
} xl:inline-flex`}
|
}`}
|
||||||
>
|
>
|
||||||
{getLabel()}
|
{itemLabel}
|
||||||
</span>
|
</span>
|
||||||
|
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
|
||||||
{item.menu && (
|
{item.menu && (
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
|
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
|
||||||
@ -90,21 +106,27 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
!isDropdownActive ? 'lg:hidden' : ''
|
!isDropdownActive ? 'lg:hidden' : ''
|
||||||
} text-sm border-b border-gray-100 lg:border lg:bg-white lg:absolute lg:top-full lg:left-0 lg:min-w-full lg:z-20 lg:rounded-lg lg:shadow-lg lg:dark:bg-slate-800 lg:dark:border-slate-700`}
|
} text-sm border-b border-gray-100 lg:border lg:bg-midnightBlueTheme-cardColor lg:absolute lg:top-full lg:left-0 lg:min-w-full lg:z-20 lg:rounded-lg lg:shadow-lg lg:dark:bg-dark-900 dark:border-dark-700`}
|
||||||
>
|
>
|
||||||
<NavBarMenuList menu={item.menu} />
|
<ClickOutside onClickOutside={() => setIsDropdownActive(false)} excludedElements={[excludedRef]}>
|
||||||
|
<NavBarMenuList menu={item.menu} />
|
||||||
|
</ClickOutside>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (item.isDivider) {
|
||||||
|
return <BaseDivider navBar />
|
||||||
|
}
|
||||||
|
|
||||||
if (item.href) {
|
if (item.href) {
|
||||||
return (
|
return (
|
||||||
<Link href={item.href} target={item.target} className={wrapperClass}>
|
<Link href={item.href} target={item.target} className={componentClass}>
|
||||||
{NavBarItemComponentContents}
|
{NavBarItemComponentContents}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={wrapperClass}>{NavBarItemComponentContents}</div>
|
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export default function PasswordSetOrReset() {
|
|||||||
type: isInvitation && 'invitation',
|
type: isInvitation && 'invitation',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await router.push('/admin-login');
|
await router.push('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -110,4 +110,4 @@ export default function PasswordSetOrReset() {
|
|||||||
<ToastContainer/>
|
<ToastContainer/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { ReactNode, useEffect, useState } from 'react'
|
import React, { ReactNode, useEffect } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
@ -52,7 +53,7 @@ export default function LayoutAuthenticated({
|
|||||||
dispatch(findMe());
|
dispatch(findMe());
|
||||||
if (!isTokenValid()) {
|
if (!isTokenValid()) {
|
||||||
dispatch(logoutUser());
|
dispatch(logoutUser());
|
||||||
router.push('/admin-login');
|
router.push('/login');
|
||||||
}
|
}
|
||||||
}, [token, localToken]);
|
}, [token, localToken]);
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: '/ai-developer',
|
|
||||||
label: 'AI Developer Portal',
|
|
||||||
icon: icon.mdiBrain,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
@ -140,4 +136,4 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { mdiKeyVariant, mdiArrowLeft, mdiShieldLock } from '@mdi/js';
|
|
||||||
import BaseIcon from '../components/BaseIcon';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
|
||||||
import { getPageTitle } from '../config';
|
|
||||||
import CardBox from '../components/CardBox';
|
|
||||||
|
|
||||||
export default function AdminPrivateKeyLogin() {
|
|
||||||
const [privateKey, setPrivateKey] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/auth/signin/private-key', { privateKey });
|
|
||||||
const { token } = response.data;
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
localStorage.setItem('token', token);
|
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
||||||
// Redirect to dashboard
|
|
||||||
router.push('/dashboard');
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.response?.data?.message || 'Invalid Private Key');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-[#020617] min-h-screen flex items-center justify-center p-6 text-slate-100">
|
|
||||||
<Head>
|
|
||||||
<title>{getPageTitle('Admin Secure Access')}</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<div className="w-full max-w-md relative">
|
|
||||||
{/* Back Link */}
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/')}
|
|
||||||
className="absolute -top-12 left-0 text-slate-400 hover:text-white flex items-center gap-2 text-sm transition-colors"
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiArrowLeft} size={14} /> Back to Gallery
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<CardBox className="bg-slate-900/80 border-slate-800 shadow-2xl shadow-violet-500/10 backdrop-blur-xl rounded-3xl p-8">
|
|
||||||
<div className="text-center mb-10">
|
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-violet-600/10 rounded-2xl mb-4 border border-violet-500/20">
|
|
||||||
<BaseIcon path={mdiShieldLock} size={36} className="text-violet-500" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">
|
|
||||||
Developer Access
|
|
||||||
</h1>
|
|
||||||
<p className="text-slate-500 text-sm mt-2">
|
|
||||||
Enter your unique blockchain-secured private key to manage the platform.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-widest ml-1">
|
|
||||||
Private Key
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
||||||
<BaseIcon path={mdiKeyVariant} size={18} className="text-slate-500" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={privateKey}
|
|
||||||
onChange={(e) => setPrivateKey(e.target.value)}
|
|
||||||
placeholder="Enter your 64-character hex key..."
|
|
||||||
className="w-full bg-slate-950 border-slate-800 text-slate-200 pl-12 pr-4 py-4 rounded-2xl focus:ring-2 focus:ring-violet-500 focus:border-transparent transition-all placeholder:text-slate-700"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-500/10 border border-red-500/20 text-red-400 text-xs p-4 rounded-xl flex items-center gap-3">
|
|
||||||
<div className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse" />
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full bg-violet-600 hover:bg-violet-700 disabled:bg-violet-800 disabled:opacity-50 text-white font-bold py-4 rounded-2xl transition-all shadow-lg shadow-violet-500/20 transform hover:scale-[1.02] active:scale-[0.98]"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
||||||
Authenticating...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Verify & Unlock'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-10 pt-8 border-t border-slate-800 text-center">
|
|
||||||
<p className="text-[10px] text-slate-600 uppercase tracking-widest">
|
|
||||||
Secure Encrypted Session • AI Game Studio v1.0
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AdminPrivateKeyLogin.getLayout = function getLayout(page: ReactElement) {
|
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
|
||||||
};
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
import { mdiBrain, mdiRocketLaunch, mdiChartTimelineVariant, mdiConsole, mdiCheckboxMarkedCircleOutline, mdiProgressClock, mdiAlertCircleOutline, mdiDownload, mdiMonitor, mdiCellphone, mdiOpenInNew } 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 { useAppDispatch, useAppSelector } from '../stores/hooks';
|
|
||||||
import { generate, fetch, setRefetch } from '../stores/ai_game_projects/ai_game_projectsSlice';
|
|
||||||
import BaseButton from '../components/BaseButton';
|
|
||||||
import FormField from '../components/FormField';
|
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseIcon from '../components/BaseIcon';
|
|
||||||
|
|
||||||
const AiDeveloperPortal = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { ai_game_projects, loading, refetch } = useAppSelector((state) => state.ai_game_projects);
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
const [concept, setConcept] = useState('');
|
|
||||||
const [projectName, setProjectName] = useState('');
|
|
||||||
const [dimension, setDimension] = useState('2d');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetch({}));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (refetch) {
|
|
||||||
dispatch(fetch({}));
|
|
||||||
dispatch(setRefetch(false));
|
|
||||||
}
|
|
||||||
}, [refetch, dispatch]);
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
|
||||||
if (!concept || !projectName) return;
|
|
||||||
dispatch(generate({
|
|
||||||
project_name: projectName,
|
|
||||||
game_concept: concept,
|
|
||||||
target_dimension: dimension
|
|
||||||
}));
|
|
||||||
setConcept('');
|
|
||||||
setProjectName('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'ready': return mdiCheckboxMarkedCircleOutline;
|
|
||||||
case 'generating':
|
|
||||||
case 'building': return mdiProgressClock;
|
|
||||||
case 'failed': return mdiAlertCircleOutline;
|
|
||||||
default: return mdiRocketLaunch;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'ready': return 'text-emerald-500';
|
|
||||||
case 'generating':
|
|
||||||
case 'building': return 'text-amber-500';
|
|
||||||
case 'failed': return 'text-red-500';
|
|
||||||
default: return 'text-slate-400';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentUser?.email !== 'admin@flatlogic.com') {
|
|
||||||
return (
|
|
||||||
<SectionMain>
|
|
||||||
<CardBox className="text-center py-20">
|
|
||||||
<BaseIcon path={mdiAlertCircleOutline} size={64} className="text-red-500 mx-auto mb-6" />
|
|
||||||
<h1 className="text-3xl font-black mb-4 uppercase">Access Restricted</h1>
|
|
||||||
<p className="text-slate-400 max-w-md mx-auto">This portal is reserved for the platform developer with the primary private key.</p>
|
|
||||||
</CardBox>
|
|
||||||
</SectionMain>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>{getPageTitle('AI Developer Portal')}</title>
|
|
||||||
</Head>
|
|
||||||
<SectionMain>
|
|
||||||
<SectionTitleLineWithButton icon={mdiBrain} title='Intelligent AI Developer' main>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-slate-400">
|
|
||||||
<BaseIcon path={mdiConsole} size={18} />
|
|
||||||
<span>v4.0.2-quantum</span>
|
|
||||||
</div>
|
|
||||||
</SectionTitleLineWithButton>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
|
||||||
<CardBox className="lg:col-span-2 shadow-2xl bg-slate-900 border-slate-800">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-2xl font-bold text-white mb-2">Initialize Autonomous Game Build</h2>
|
|
||||||
<p className="text-slate-400">The engine will generate the logic, assets, and compiled executables for all platforms.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<FormField label="Project Name" help="Target identifier for the compiled binary">
|
|
||||||
<input
|
|
||||||
value={projectName}
|
|
||||||
onChange={(e) => setProjectName(e.target.value)}
|
|
||||||
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-violet-500 outline-none"
|
|
||||||
placeholder="e.g. Cyberpunk Odyssey"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label="Compilation Target">
|
|
||||||
<select
|
|
||||||
value={dimension}
|
|
||||||
onChange={(e) => setDimension(e.target.value)}
|
|
||||||
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-violet-500 outline-none"
|
|
||||||
>
|
|
||||||
<option value="2d">2D High-Resolution (Universal APK/EXE)</option>
|
|
||||||
<option value="3d">3D Immersive Environment (Universal APK/EXE)</option>
|
|
||||||
</select>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label="Functional Specifications" help="Define the logic flow. The AI will compile these into active code.">
|
|
||||||
<textarea
|
|
||||||
value={concept}
|
|
||||||
onChange={(e) => setConcept(e.target.value)}
|
|
||||||
className="w-full h-40 bg-slate-800 border-slate-700 text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-violet-500 outline-none resize-none"
|
|
||||||
placeholder="Describe mechanics, win/loss conditions, and UI flow..."
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<BaseButton
|
|
||||||
label={loading ? "Compiling..." : "Generate Functional Game"}
|
|
||||||
color="info"
|
|
||||||
icon={mdiRocketLaunch}
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={loading || !concept || !projectName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
<CardBox className="bg-slate-900 border-slate-800">
|
|
||||||
<h3 className="text-xl font-bold text-white mb-4">Compilation Engine</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-slate-800 rounded-lg">
|
|
||||||
<div className="flex justify-between mb-2">
|
|
||||||
<span className="text-sm text-slate-300">GPU Utilization</span>
|
|
||||||
<span className="text-sm text-cyan-400 font-mono">82%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-700 h-1 rounded-full overflow-hidden">
|
|
||||||
<div className="bg-cyan-400 h-full w-[82%]"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-slate-800 rounded-lg">
|
|
||||||
<div className="flex justify-between mb-2">
|
|
||||||
<span className="text-sm text-slate-300">Binary Packing</span>
|
|
||||||
<span className="text-sm text-violet-400 font-mono">Active</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-700 h-1 rounded-full overflow-hidden">
|
|
||||||
<div className="bg-violet-400 h-full w-full"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3 text-sm text-slate-400 italic">
|
|
||||||
<BaseIcon path={mdiBrain} size={24} className="text-violet-500 flex-shrink-0" />
|
|
||||||
<p>"The platform handles the complexity. You provide the intent, we provide the executable."</p>
|
|
||||||
</div>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Deployment Pipeline' />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
|
||||||
{ai_game_projects && ai_game_projects.length > 0 ? (
|
|
||||||
ai_game_projects.map((project: any) => (
|
|
||||||
<CardBox key={project.id} className="bg-slate-900 border-slate-800 hover:border-violet-500 transition-colors">
|
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className={`p-3 rounded-xl bg-slate-800 ${getStatusColor(project.project_status)}`}>
|
|
||||||
<BaseIcon path={getStatusIcon(project.project_status)} size={32} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-lg font-bold text-white">{project.project_name}</h4>
|
|
||||||
<p className="text-sm text-slate-400 uppercase tracking-widest">{project.target_dimension} • Created {new Date(project.createdAt).toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 md:mt-0 flex items-center space-x-4">
|
|
||||||
{project.project_status === 'ready' && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<BaseButton
|
|
||||||
label="Play (Browser)"
|
|
||||||
color="success"
|
|
||||||
small
|
|
||||||
icon={mdiOpenInNew}
|
|
||||||
onClick={() => window.open(project.play_url, '_blank')}
|
|
||||||
/>
|
|
||||||
<BaseButton
|
|
||||||
label="Download PC"
|
|
||||||
color="info"
|
|
||||||
small
|
|
||||||
icon={mdiMonitor}
|
|
||||||
onClick={() => window.open(project.download_url_pc, '_blank')}
|
|
||||||
/>
|
|
||||||
<BaseButton
|
|
||||||
label="Android APK"
|
|
||||||
color="whiteDark"
|
|
||||||
small
|
|
||||||
icon={mdiCellphone}
|
|
||||||
onClick={() => window.open(project.download_url_mobile, '_blank')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-right hidden md:block">
|
|
||||||
<div className={`text-sm font-bold uppercase ${getStatusColor(project.project_status)}`}>
|
|
||||||
{project.project_status}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(project.project_status === 'generating' || project.project_status === 'building') && (
|
|
||||||
<div className="mt-4 w-full bg-slate-800 h-1 rounded-full overflow-hidden">
|
|
||||||
<div className="bg-violet-500 h-full animate-pulse w-full"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardBox>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12 bg-slate-900 border-2 border-dashed border-slate-800 rounded-3xl">
|
|
||||||
<BaseIcon path={mdiRocketLaunch} size={48} className="mx-auto text-slate-700 mb-4" />
|
|
||||||
<p className="text-slate-500">Deployment pipeline empty. Initializing engine...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SectionMain>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AiDeveloperPortal.getLayout = function getLayout(page: ReactElement) {
|
|
||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AiDeveloperPortal;
|
|
||||||
82
frontend/src/pages/forgot.tsx
Normal file
82
frontend/src/pages/forgot.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React 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 axios from "axios";
|
||||||
|
|
||||||
|
export default function Forgot() {
|
||||||
|
const [loading, setLoading] = React.useState(false)
|
||||||
|
const router = useRouter();
|
||||||
|
const notify = (type, msg) => toast( msg, {type});
|
||||||
|
|
||||||
|
const handleSubmit = async (value) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
|
||||||
|
setLoading(false)
|
||||||
|
notify('success', 'Please check your email for verification link');
|
||||||
|
setTimeout(async () => {
|
||||||
|
await router.push('/login')
|
||||||
|
}, 3000)
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false)
|
||||||
|
console.log('error: ', error)
|
||||||
|
notify('error', 'Something was wrong. Try again')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Login')}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<SectionFullScreen bg='violet'>
|
||||||
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
email: '',
|
||||||
|
}}
|
||||||
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormField label='Email' help='Please enter your email'>
|
||||||
|
<Field name='email' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton
|
||||||
|
type='submit'
|
||||||
|
label={loading ? 'Loading...' : 'Submit' }
|
||||||
|
color='info'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
href={'/login'}
|
||||||
|
label={'Login'}
|
||||||
|
color='info'
|
||||||
|
/>
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</SectionFullScreen>
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Forgot.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
|
};
|
||||||
@ -1,502 +1,166 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
|
||||||
import Head from 'next/head'
|
|
||||||
import LayoutGuest from '../layouts/Guest'
|
|
||||||
import { getPageTitle } from '../config'
|
|
||||||
import BaseButton from '../components/BaseButton'
|
|
||||||
import {
|
|
||||||
mdiGamepadVariant,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiQrcode,
|
|
||||||
mdiRocketLaunch,
|
|
||||||
mdiShieldCheck,
|
|
||||||
mdiBrain,
|
|
||||||
mdiLockOutline,
|
|
||||||
mdiCheckCircle,
|
|
||||||
mdiKey,
|
|
||||||
mdiArrowRight,
|
|
||||||
mdiRefresh,
|
|
||||||
mdiCircle
|
|
||||||
} from '@mdi/js'
|
|
||||||
import BaseIcon from '../components/BaseIcon'
|
|
||||||
import axios from 'axios'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
export default function IndexPage() {
|
import React, { useEffect, useState } from 'react';
|
||||||
const [games, setGames] = useState([])
|
import type { ReactElement } from 'react';
|
||||||
const [categories, setCategories] = useState([])
|
import Head from 'next/head';
|
||||||
const [activeCategory, setActiveCategory] = useState('all')
|
import Link from 'next/link';
|
||||||
const [selectedGame, setSelectedGame] = useState<any>(null)
|
import BaseButton from '../components/BaseButton';
|
||||||
const [selectedOption, setSelectedOption] = useState<any>(null)
|
import CardBox from '../components/CardBox';
|
||||||
const [isPurchasing, setIsPurchasing] = useState(false)
|
import SectionFullScreen from '../components/SectionFullScreen';
|
||||||
const [isUnlocked, setIsUnlocked] = useState(false)
|
import LayoutGuest from '../layouts/Guest';
|
||||||
const [accessCode, setAccessCode] = useState('')
|
import BaseDivider from '../components/BaseDivider';
|
||||||
const [showCodeGate, setShowCodeGate] = useState(true)
|
import BaseButtons from '../components/BaseButtons';
|
||||||
const [generatedCode, setGeneratedCode] = useState('')
|
import { getPageTitle } from '../config';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||||
|
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||||
|
|
||||||
const pricingOptions = [
|
|
||||||
{ label: '1 DAY', price: '5,00', duration: '1 day' },
|
|
||||||
{ label: '3 DAYS', price: '12,00', duration: '3 days' },
|
|
||||||
{ label: '1 WEEK', price: '20,00', duration: '1 week' },
|
|
||||||
{ label: '1 MONTH', price: '50,00', duration: '1 month' },
|
|
||||||
{ label: '3 MONTHS', price: '120,00', duration: '3 months' }
|
|
||||||
]
|
|
||||||
|
|
||||||
useEffect(() => {
|
export default function Starter() {
|
||||||
const savedCode = localStorage.getItem('accessCode')
|
const [illustrationImage, setIllustrationImage] = useState({
|
||||||
if (savedCode) {
|
src: undefined,
|
||||||
setAccessCode(savedCode)
|
photographer: undefined,
|
||||||
setShowCodeGate(false)
|
photographer_url: undefined,
|
||||||
}
|
})
|
||||||
|
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||||
|
const [contentType, setContentType] = useState('video');
|
||||||
|
const [contentPosition, setContentPosition] = useState('right');
|
||||||
|
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const title = 'AI Game Studio Marketplace'
|
||||||
try {
|
|
||||||
const [gamesRes, catsRes] = await Promise.all([
|
|
||||||
axios.get('/games'),
|
|
||||||
axios.get('/game_categories')
|
|
||||||
])
|
|
||||||
setGames(gamesRes.data.rows || [])
|
|
||||||
setCategories(catsRes.data.rows || [])
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch landing data", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const generateNewCode = () => {
|
// Fetch Pexels image/video
|
||||||
const code = Math.floor(100000 + Math.random() * 900000).toString()
|
useEffect(() => {
|
||||||
setGeneratedCode(code)
|
async function fetchData() {
|
||||||
}
|
const image = await getPexelsImage();
|
||||||
|
const video = await getPexelsVideo();
|
||||||
const handleCodeSubmit = async (codeToUse?: string) => {
|
setIllustrationImage(image);
|
||||||
const code = codeToUse || accessCode
|
setIllustrationVideo(video);
|
||||||
if (code.length === 6) {
|
|
||||||
try {
|
|
||||||
await axios.post('/auth/signin/access-code', { code })
|
|
||||||
localStorage.setItem('accessCode', code)
|
|
||||||
setAccessCode(code)
|
|
||||||
setShowCodeGate(false)
|
|
||||||
} catch (err) {
|
|
||||||
// Silently allow access for the prototype if backend is being updated
|
|
||||||
localStorage.setItem('accessCode', code)
|
|
||||||
setAccessCode(code)
|
|
||||||
setShowCodeGate(false)
|
|
||||||
}
|
}
|
||||||
}
|
fetchData();
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
const checkAccess = async (gameId: string) => {
|
const imageBlock = (image) => (
|
||||||
try {
|
<div
|
||||||
const res = await axios.get(`/games/verify-access?gameId=${gameId}&guestId=${accessCode}`)
|
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||||
setIsUnlocked(res.data)
|
style={{
|
||||||
} catch (err) {
|
backgroundImage: `${
|
||||||
console.error("Failed to verify access", err)
|
image
|
||||||
}
|
? `url(${image?.src?.original})`
|
||||||
}
|
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||||
|
}`,
|
||||||
useEffect(() => {
|
backgroundSize: 'cover',
|
||||||
if (selectedGame && accessCode) {
|
backgroundPosition: 'left center',
|
||||||
checkAccess(selectedGame.id)
|
backgroundRepeat: 'no-repeat',
|
||||||
}
|
}}
|
||||||
}, [selectedGame, accessCode])
|
>
|
||||||
|
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||||
const handlePurchase = async () => {
|
<a
|
||||||
if (!selectedGame || !selectedOption) return
|
className='text-[8px]'
|
||||||
setIsPurchasing(true)
|
href={image?.photographer_url}
|
||||||
try {
|
target='_blank'
|
||||||
await axios.post('/games/purchase', {
|
rel='noreferrer'
|
||||||
gameId: selectedGame.id,
|
>
|
||||||
timePassId: selectedOption.label,
|
Photo by {image?.photographer} on Pexels
|
||||||
guestId: accessCode
|
</a>
|
||||||
})
|
|
||||||
setIsUnlocked(true)
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Purchase failed", err)
|
|
||||||
setIsUnlocked(true) // Simulate success for demo
|
|
||||||
} finally {
|
|
||||||
setIsPurchasing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredGames = activeCategory === 'all'
|
|
||||||
? games
|
|
||||||
: games.filter((g: any) => g.game_categoryId === activeCategory)
|
|
||||||
|
|
||||||
if (showCodeGate) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[#020617] text-white flex items-center justify-center p-6 selection:bg-violet-500/30 font-sans">
|
|
||||||
<Head>
|
|
||||||
<title>Nexus Access Gate</title>
|
|
||||||
</Head>
|
|
||||||
<div className="w-full max-w-lg">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-violet-600 to-cyan-500 rounded-3xl mb-6 shadow-2xl shadow-violet-500/20">
|
|
||||||
<BaseIcon path={mdiGamepadVariant} size={40} color="white" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-4xl font-black tracking-tighter uppercase italic mb-2">Nexus<span className="text-violet-500">Games</span></h1>
|
|
||||||
<p className="text-slate-500 text-sm font-bold uppercase tracking-widest">Premium AI Game Platform</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-slate-900/50 backdrop-blur-3xl border border-white/10 rounded-[40px] p-8 md:p-10 shadow-2xl shadow-black">
|
|
||||||
{!generatedCode ? (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-bold mb-2">Enter Access Code</h2>
|
|
||||||
<p className="text-slate-400 text-sm">Please provide your 6-digit common access code to enter the gallery.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
maxLength={6}
|
|
||||||
value={accessCode}
|
|
||||||
onChange={(e) => setAccessCode(e.target.value.replace(/\D/g, ''))}
|
|
||||||
placeholder="0 0 0 0 0 0"
|
|
||||||
className="w-full max-w-xs bg-black/50 border-2 border-slate-800 rounded-2xl py-5 text-4xl text-center font-black tracking-[0.5em] text-violet-500 focus:border-violet-600 outline-none transition-all placeholder:text-slate-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseButton
|
|
||||||
label="Enter Gallery"
|
|
||||||
color="info"
|
|
||||||
className="w-full py-5 rounded-2xl text-lg font-bold"
|
|
||||||
disabled={accessCode.length !== 6}
|
|
||||||
onClick={() => handleCodeSubmit()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative flex items-center justify-center py-4">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-white/5"></div>
|
|
||||||
</div>
|
|
||||||
<span className="relative bg-[#0b1120] px-4 text-[10px] font-black text-slate-600 uppercase tracking-widest">New User?</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={generateNewCode}
|
|
||||||
className="w-full flex items-center justify-center space-x-2 text-slate-400 hover:text-white transition-colors text-sm font-bold"
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiRefresh} size={18} />
|
|
||||||
<span>Generate New Access Code</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-8 text-center">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold mb-2 text-emerald-400">Code Generated!</h2>
|
|
||||||
<p className="text-slate-400 text-sm">Save this 6-digit code. Use it anytime to access the games area and your purchases.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-3xl p-10">
|
|
||||||
<span className="text-6xl font-black tracking-[0.2em] text-emerald-400">{generatedCode}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<BaseButton
|
|
||||||
label="Continue to Platform"
|
|
||||||
color="success"
|
|
||||||
className="w-full py-5 rounded-2xl text-lg font-bold"
|
|
||||||
onClick={() => handleCodeSubmit(generatedCode)}
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Military-Grade Encryption Active</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-12 text-center">
|
|
||||||
<Link href="/admin-login" className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-700 hover:text-violet-500 transition-colors">Developer Portal Login</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
const videoBlock = (video) => {
|
||||||
|
if (video?.video_files?.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||||
|
<video
|
||||||
|
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
>
|
||||||
|
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||||
|
<a
|
||||||
|
className='text-[8px]'
|
||||||
|
href={video?.user?.url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
Video by {video.user.name} on Pexels
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#020617] text-white selection:bg-violet-500/30">
|
<div
|
||||||
|
style={
|
||||||
|
contentPosition === 'background'
|
||||||
|
? {
|
||||||
|
backgroundImage: `${
|
||||||
|
illustrationImage
|
||||||
|
? `url(${illustrationImage.src?.original})`
|
||||||
|
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||||
|
}`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'left center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Nexus Gaming - Premium AI Game Platform')}</title>
|
<title>{getPageTitle('Starter Page')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
{/* Navbar */}
|
<SectionFullScreen bg='violet'>
|
||||||
<nav className="flex items-center justify-between px-6 py-4 border-b border-white/5 backdrop-blur-md sticky top-0 z-50">
|
<div
|
||||||
<div className="flex items-center space-x-2">
|
className={`flex ${
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-violet-600 to-cyan-500 rounded-xl flex items-center justify-center shadow-lg shadow-violet-500/20">
|
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||||
<BaseIcon path={mdiGamepadVariant} size={24} color="white" />
|
} min-h-screen w-full`}
|
||||||
</div>
|
>
|
||||||
<span className="text-xl font-black tracking-tighter uppercase italic">Nexus<span className="text-violet-500">Games</span></span>
|
{contentType === 'image' && contentPosition !== 'background'
|
||||||
</div>
|
? imageBlock(illustrationImage)
|
||||||
<div className="hidden md:flex items-center space-x-8 text-sm font-medium text-slate-400">
|
: null}
|
||||||
<a href="#games" className="hover:text-white transition-colors">Gallery</a>
|
{contentType === 'video' && contentPosition !== 'background'
|
||||||
<a href="#payment" className="hover:text-white transition-colors">Buy Access</a>
|
? videoBlock(illustrationVideo)
|
||||||
<button
|
: null}
|
||||||
onClick={() => {
|
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||||
localStorage.removeItem('accessCode');
|
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||||
setShowCodeGate(true);
|
<CardBoxComponentTitle title="Welcome to your AI Game Studio Marketplace app!"/>
|
||||||
}}
|
|
||||||
className="hover:text-white transition-colors text-xs font-black uppercase tracking-widest"
|
<div className="space-y-3">
|
||||||
>
|
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||||
Switch User ({accessCode})
|
<p className='text-center '>For guides and documentation please check
|
||||||
</button>
|
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="px-4 py-2 bg-violet-600/10 border border-violet-500/20 rounded-full text-xs font-black text-violet-400 tracking-widest flex items-center space-x-2">
|
|
||||||
<BaseIcon path={mdiCircle} size={10} color="currentColor" className="animate-pulse" />
|
|
||||||
<span>CODE: {accessCode}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className="relative pt-20 pb-32 px-6 overflow-hidden">
|
|
||||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[600px] bg-violet-600/10 blur-[120px] rounded-full -z-10"></div>
|
|
||||||
<div className="max-w-6xl mx-auto text-center">
|
|
||||||
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-400 text-[10px] font-black uppercase tracking-widest mb-6">
|
|
||||||
<BaseIcon path={mdiShieldCheck} size={14} />
|
|
||||||
<span>Intelligent Game Development & Distribution Ecosystem</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-none mb-8 uppercase italic">
|
|
||||||
NEXUS <span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-cyan-400">GAMES</span><br/>
|
|
||||||
GALLERY
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-2xl font-bold text-slate-400 mb-10">PREMIUM AI-POWERED GAMING</h2>
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
|
||||||
<BaseButton label="Explore Games" color="info" icon={mdiGamepadVariant} className="px-10 py-5 text-xl font-black italic rounded-2xl" href="#games" />
|
|
||||||
<Link href="/admin-login" className="px-10 py-5 bg-white/5 border border-white/10 rounded-2xl text-xl font-black italic hover:bg-white/10 transition-all flex items-center space-x-2">
|
|
||||||
<BaseIcon path={mdiBrain} size={24} />
|
|
||||||
<span>AI Developer Portal</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Game Gallery */}
|
|
||||||
<section id="games" className="px-6 py-24 max-w-7xl mx-auto">
|
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-16 space-y-8 md:space-y-0">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-5xl font-black mb-4 tracking-tighter uppercase italic">Nexus <span className="text-violet-500">Gallery</span></h2>
|
|
||||||
<div className="flex items-center text-slate-500 text-xs font-bold uppercase tracking-[0.2em]">
|
|
||||||
<div className="w-2 h-2 bg-violet-500 rounded-full mr-3 animate-ping"></div>
|
|
||||||
Streaming Live Content
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex items-center bg-white/5 p-1.5 rounded-2xl border border-white/5 overflow-x-auto whitespace-nowrap">
|
<BaseButtons>
|
||||||
<button
|
<BaseButton
|
||||||
onClick={() => setActiveCategory('all')}
|
href='/login'
|
||||||
className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${activeCategory === 'all' ? 'bg-violet-600 text-white shadow-xl' : 'text-slate-500 hover:text-white'}`}
|
label='Login'
|
||||||
>
|
color='info'
|
||||||
All Genres
|
className='w-full'
|
||||||
</button>
|
|
||||||
{categories.map((cat: any) => (
|
|
||||||
<button
|
|
||||||
key={cat.id}
|
|
||||||
onClick={() => setActiveCategory(cat.id)}
|
|
||||||
className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${activeCategory === cat.id ? 'bg-violet-600 text-white shadow-xl' : 'text-slate-500 hover:text-white'}`}
|
|
||||||
>
|
|
||||||
{cat.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
||||||
{filteredGames.map((game: any) => (
|
|
||||||
<div
|
|
||||||
key={game.id}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedGame(game);
|
|
||||||
setSelectedOption(null);
|
|
||||||
window.location.href = '#payment';
|
|
||||||
}}
|
|
||||||
className={`group relative bg-[#0b1120] rounded-[40px] overflow-hidden border transition-all duration-500 cursor-pointer ${selectedGame?.id === game.id ? 'border-violet-500 ring-4 ring-violet-500/20 scale-95' : 'border-white/5 hover:border-violet-500/40 hover:-translate-y-2'}`}
|
|
||||||
>
|
|
||||||
<div className="aspect-[4/5] overflow-hidden">
|
|
||||||
<img
|
|
||||||
src={game.game_image || 'https://images.pexels.com/photos/3165335/pexels-photo-3165335.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'}
|
|
||||||
alt={game.title}
|
|
||||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-[#020617] via-[#020617]/20 to-transparent p-8 flex flex-col justify-end">
|
</BaseButtons>
|
||||||
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-violet-400 mb-2">{categories.find((c: any) => c.id === game.game_categoryId)?.name || 'Premium Experience'}</span>
|
</CardBox>
|
||||||
<h3 className="text-3xl font-black mb-3 italic uppercase tracking-tighter leading-none">{game.title}</h3>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center text-[10px] font-black uppercase tracking-widest text-slate-500">
|
|
||||||
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mr-2 shadow-lg shadow-emerald-500/50"></div>
|
|
||||||
ACTIVE_SERVER
|
|
||||||
</div>
|
|
||||||
<BaseIcon path={mdiArrowRight} size={20} className="text-white opacity-0 group-hover:opacity-100 -translate-x-4 group-hover:translate-x-0 transition-all" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</SectionFullScreen>
|
||||||
|
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||||
|
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||||
|
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Integrated Selection & Payment */}
|
|
||||||
{selectedGame && (
|
|
||||||
<section id="payment" className="px-6 py-32 bg-[#020617] border-y border-white/5 relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-violet-600/5 blur-[120px] rounded-full"></div>
|
|
||||||
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-cyan-600/5 blur-[120px] rounded-full"></div>
|
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto relative z-10">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20 items-start">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center space-x-8 mb-16">
|
|
||||||
<div className="w-28 h-28 rounded-[32px] overflow-hidden border border-white/10 shadow-2xl rotate-3 scale-110">
|
|
||||||
<img src={selectedGame.game_image} alt="" className="w-full h-full object-cover" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-5xl font-black italic uppercase tracking-tighter mb-2">{selectedGame.title}</h3>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<span className="px-3 py-1 bg-violet-600/20 text-violet-400 text-[10px] font-black uppercase tracking-[0.2em] rounded-lg border border-violet-500/20">4K ENGINE</span>
|
|
||||||
<span className="px-3 py-1 bg-cyan-600/20 text-cyan-400 text-[10px] font-black uppercase tracking-[0.2em] rounded-lg border border-cyan-500/20">INSTANT_PLAY</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isUnlocked ? (
|
|
||||||
<div className="bg-emerald-500/5 border border-emerald-500/10 p-12 rounded-[56px] text-center shadow-2xl shadow-emerald-500/5 backdrop-blur-3xl">
|
|
||||||
<div className="w-28 h-28 bg-emerald-500 rounded-full flex items-center justify-center mx-auto mb-10 shadow-2xl shadow-emerald-500/30">
|
|
||||||
<BaseIcon path={mdiCheckCircle} size={64} color="white" />
|
|
||||||
</div>
|
|
||||||
<h4 className="text-4xl font-black mb-4 uppercase italic tracking-tighter">Access Authorized</h4>
|
|
||||||
<p className="text-slate-400 mb-12 font-medium text-lg leading-relaxed">System successfully linked to your access code.<br/><span className="text-emerald-400 font-black">Ready for deployment.</span></p>
|
|
||||||
<BaseButton
|
|
||||||
label="Initialize System"
|
|
||||||
color="success"
|
|
||||||
icon={mdiRocketLaunch}
|
|
||||||
className="w-full py-8 text-2xl font-black uppercase tracking-tighter rounded-[32px] shadow-2xl shadow-emerald-500/20 hover:scale-[1.02] active:scale-[0.98] transition-all"
|
|
||||||
onClick={() => window.open(selectedGame.web_play_url, '_blank')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-10">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-xs font-black uppercase tracking-[0.4em] mb-8 flex items-center text-slate-500">
|
|
||||||
<BaseIcon path={mdiClockOutline} size={18} className="mr-4 text-violet-500" />
|
|
||||||
Select Time-Pass Duration
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
{pricingOptions.map((opt: any) => (
|
|
||||||
<div
|
|
||||||
key={opt.label}
|
|
||||||
onClick={() => setSelectedOption(opt)}
|
|
||||||
className={`p-7 rounded-[32px] border-2 cursor-pointer transition-all duration-300 ${selectedOption?.label === opt.label ? 'bg-violet-600 border-violet-400 shadow-2xl shadow-violet-500/30 translate-x-2' : 'bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/10'}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start mb-6">
|
|
||||||
<div className="text-2xl font-black uppercase italic tracking-tighter">{opt.label}</div>
|
|
||||||
{selectedOption?.label === opt.label && (
|
|
||||||
<div className="w-6 h-6 bg-white rounded-full flex items-center justify-center">
|
|
||||||
<BaseIcon path={mdiCheckCircle} size={18} className="text-violet-600" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end justify-between">
|
|
||||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Global Access</div>
|
|
||||||
<div className="text-3xl font-black text-white">R$ {opt.price}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-8 bg-violet-600/5 rounded-[32px] border border-violet-500/10 flex items-center justify-between backdrop-blur-md">
|
|
||||||
<div className="flex items-center space-x-6">
|
|
||||||
<div className="w-14 h-14 bg-violet-600/20 rounded-2xl flex items-center justify-center">
|
|
||||||
<BaseIcon path={mdiLockOutline} size={28} className="text-violet-500" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1">Authenticated ID</div>
|
|
||||||
<div className="text-lg font-mono font-bold text-violet-400 tracking-tighter">NEXUS_USR_{accessCode}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:block">
|
|
||||||
<BaseIcon path={mdiShieldCheck} size={32} className="text-emerald-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isUnlocked && (
|
|
||||||
<div className="bg-gradient-to-br from-violet-600 via-violet-500 to-cyan-500 p-1.5 rounded-[60px] shadow-2xl shadow-violet-500/20 group">
|
|
||||||
<div className="bg-[#020617] rounded-[58px] p-12 md:p-16 h-full flex flex-col items-center">
|
|
||||||
<div className="text-center mb-16">
|
|
||||||
<h3 className="text-4xl font-black mb-4 uppercase italic tracking-tighter">Official Payment</h3>
|
|
||||||
<p className="text-slate-500 text-xs font-black uppercase tracking-[0.3em]">Scan QR Code for Instant Access</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center mb-16 w-full">
|
|
||||||
<div className="flex flex-col items-center group/qr w-full max-w-sm">
|
|
||||||
<div className="w-full aspect-square bg-white p-5 rounded-[48px] mb-6 shadow-2xl shadow-white/5 transition-transform duration-500 group-hover/qr:scale-105">
|
|
||||||
<img src="/payment-qr.jpg" alt="Payment QR" className="w-full h-full object-contain" />
|
|
||||||
</div>
|
|
||||||
<span className="text-xs font-black uppercase tracking-[0.3em] text-cyan-400 italic">Official Payment Network</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full space-y-8">
|
|
||||||
<div className="text-center flex items-center justify-center space-x-4 mb-4">
|
|
||||||
<div className="flex space-x-1.5">
|
|
||||||
<div className="w-2 h-2 bg-violet-500 rounded-full animate-bounce"></div>
|
|
||||||
<div className="w-2 h-2 bg-violet-500 rounded-full animate-bounce delay-100"></div>
|
|
||||||
<div className="w-2 h-2 bg-violet-500 rounded-full animate-bounce delay-200"></div>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-slate-500">Awaiting Confirmation</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
disabled={!selectedOption || isPurchasing}
|
|
||||||
onClick={handlePurchase}
|
|
||||||
className="w-full group relative"
|
|
||||||
>
|
|
||||||
<div className="absolute -inset-1.5 bg-gradient-to-r from-violet-600 to-cyan-500 rounded-3xl blur opacity-30 group-hover:opacity-100 transition duration-500"></div>
|
|
||||||
<div className="relative px-8 py-6 bg-violet-600 rounded-3xl leading-none flex items-center justify-center space-x-4 group-disabled:opacity-50">
|
|
||||||
<BaseIcon path={mdiQrcode} size={28} color="white" />
|
|
||||||
<span className="text-xl font-black uppercase italic tracking-tighter text-white">
|
|
||||||
{isPurchasing ? 'Processing Request...' : 'Confirm Payment'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center space-x-8 opacity-30">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<BaseIcon path={mdiShieldCheck} size={16} />
|
|
||||||
<span className="text-[8px] font-black uppercase tracking-widest">Encrypted</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<BaseIcon path={mdiLockOutline} size={16} />
|
|
||||||
<span className="text-[8px] font-black uppercase tracking-widest">Authorized</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="px-6 py-24 border-t border-white/5 text-center bg-black">
|
|
||||||
<div className="flex items-center justify-center space-x-3 mb-12">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-violet-600 to-cyan-500 rounded-2xl flex items-center justify-center shadow-xl shadow-violet-500/20">
|
|
||||||
<BaseIcon path={mdiGamepadVariant} size={24} color="white" />
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-black tracking-tighter uppercase italic">Nexus<span className="text-violet-500">Games</span></span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap justify-center gap-10 mb-16 text-[10px] font-black uppercase tracking-[0.4em] text-slate-600">
|
|
||||||
<a href="#" className="hover:text-white transition-colors">Infrastructure</a>
|
|
||||||
<a href="#" className="hover:text-white transition-colors">Neural Engine</a>
|
|
||||||
<a href="#" className="hover:text-white transition-colors">Legal</a>
|
|
||||||
<a href="#" className="hover:text-white transition-colors">Connect</a>
|
|
||||||
</div>
|
|
||||||
<p className="text-slate-800 text-[10px] tracking-[0.6em] uppercase font-black">© 2026 NEXUS GAMING ECOSYSTEM • QUANTUM SECURED PLATFORM</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IndexPage.getLayout = function getLayout(page: ReactElement) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
276
frontend/src/pages/login.tsx
Normal file
276
frontend/src/pages/login.tsx
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
|
||||||
|
|
||||||
|
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 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, 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'
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
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 [ illustrationImage, setIllustrationImage ] = useState({
|
||||||
|
src: undefined,
|
||||||
|
photographer: undefined,
|
||||||
|
photographer_url: undefined,
|
||||||
|
})
|
||||||
|
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: '8ab6346a',
|
||||||
|
remember: true })
|
||||||
|
|
||||||
|
const title = 'AI Game Studio Marketplace'
|
||||||
|
|
||||||
|
// Fetch Pexels image/video
|
||||||
|
useEffect( () => {
|
||||||
|
async function fetchData() {
|
||||||
|
const image = await getPexelsImage()
|
||||||
|
const video = await getPexelsVideo()
|
||||||
|
setIllustrationImage(image);
|
||||||
|
setIllustrationVideo(video);
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
// Fetch user data
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
dispatch(findMe());
|
||||||
|
}
|
||||||
|
}, [token, dispatch]);
|
||||||
|
// Redirect to dashboard if user is logged in
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.id) {
|
||||||
|
router.push('/dashboard');
|
||||||
|
}
|
||||||
|
}, [currentUser?.id, router]);
|
||||||
|
// Show error message if there is one
|
||||||
|
useEffect(() => {
|
||||||
|
if (errorMessage){
|
||||||
|
notify('error', errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [errorMessage])
|
||||||
|
// Show notification if there is one
|
||||||
|
useEffect(() => {
|
||||||
|
if (notifyState?.showNotification) {
|
||||||
|
notify('success', notifyState?.textNotification)
|
||||||
|
dispatch(resetAction());
|
||||||
|
}
|
||||||
|
}, [notifyState?.showNotification])
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (value) => {
|
||||||
|
const {remember, ...rest} = value
|
||||||
|
await dispatch(loginUser(rest));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLogin = (target: HTMLElement) => {
|
||||||
|
setInitialValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
email : target.innerText.trim(),
|
||||||
|
password: target.dataset.password ?? '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageBlock = (image) => (
|
||||||
|
<div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `${image ? `url(${image.src?.original})` : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'}`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'left center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
}}>
|
||||||
|
<div className="flex justify-center w-full bg-blue-300/20">
|
||||||
|
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
|
||||||
|
by {image?.photographer} on Pexels</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const videoBlock = (video) => {
|
||||||
|
if (video?.video_files?.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||||
|
<video
|
||||||
|
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
>
|
||||||
|
<source src={video.video_files[0]?.link} type='video/mp4'/>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||||
|
<a
|
||||||
|
className='text-[8px]'
|
||||||
|
href={video.user.url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
Video by {video.user.name} on Pexels
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={contentPosition === 'background' ? {
|
||||||
|
backgroundImage: `${
|
||||||
|
illustrationImage
|
||||||
|
? `url(${illustrationImage.src?.original})`
|
||||||
|
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||||
|
}`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'left center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
} : {}}>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Login')}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<SectionFullScreen bg='violet'>
|
||||||
|
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
|
||||||
|
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
|
||||||
|
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
|
||||||
|
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||||
|
|
||||||
|
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
|
||||||
|
|
||||||
|
<h2 className="text-4xl font-semibold my-4">{title}</h2>
|
||||||
|
|
||||||
|
<div className='flex flex-row justify-between'>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<p className='mb-2'>Use{' '}
|
||||||
|
<code className={`cursor-pointer ${textColor} `}
|
||||||
|
data-password="8ab6346a"
|
||||||
|
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
||||||
|
<code className={`${textColor}`}>8ab6346a</code>{' / '}
|
||||||
|
to login as Admin</p>
|
||||||
|
<p>Use <code
|
||||||
|
className={`cursor-pointer ${textColor} `}
|
||||||
|
data-password="c3af9d1c13ec"
|
||||||
|
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
||||||
|
<code className={`${textColor}`}>c3af9d1c13ec</code>{' / '}
|
||||||
|
to login as User</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<BaseIcon
|
||||||
|
className={`${iconsColor}`}
|
||||||
|
w='w-16'
|
||||||
|
h='h-16'
|
||||||
|
size={48}
|
||||||
|
path={mdiInformation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormField
|
||||||
|
label='Login'
|
||||||
|
help='Please enter your login'>
|
||||||
|
<Field name='email' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className='relative'>
|
||||||
|
<FormField
|
||||||
|
label='Password'
|
||||||
|
help='Please enter your password'>
|
||||||
|
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
||||||
|
</FormField>
|
||||||
|
<div
|
||||||
|
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
|
||||||
|
onClick={togglePasswordVisibility}
|
||||||
|
>
|
||||||
|
<BaseIcon
|
||||||
|
className='text-gray-500 hover:text-gray-700'
|
||||||
|
size={20}
|
||||||
|
path={showPassword ? mdiEyeOff : mdiEye}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'flex justify-between'}>
|
||||||
|
<FormCheckRadio type='checkbox' label='Remember'>
|
||||||
|
<Field type='checkbox' name='remember' />
|
||||||
|
</FormCheckRadio>
|
||||||
|
|
||||||
|
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton
|
||||||
|
className={'w-full'}
|
||||||
|
type='submit'
|
||||||
|
label={isFetching ? 'Loading...' : 'Login'}
|
||||||
|
color='info'
|
||||||
|
disabled={isFetching}
|
||||||
|
/>
|
||||||
|
</BaseButtons>
|
||||||
|
<br />
|
||||||
|
<p className={'text-center'}>
|
||||||
|
Don’t have an account yet?{' '}
|
||||||
|
<Link className={`${textColor}`} href={'/register'}>
|
||||||
|
New Account
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionFullScreen>
|
||||||
|
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||||
|
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
|
||||||
|
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ToastContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Login.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
|
};
|
||||||
92
frontend/src/pages/register.tsx
Normal file
92
frontend/src/pages/register.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import React 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 axios from "axios";
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
|
||||||
|
|
||||||
|
|
||||||
|
const handleSubmit = async (value) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
|
||||||
|
const { data: response } = await axios.post('/auth/signup',value);
|
||||||
|
await router.push('/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')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Login')}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<SectionFullScreen bg='violet'>
|
||||||
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirm: ''
|
||||||
|
}}
|
||||||
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
|
||||||
|
<FormField label='Email' help='Please enter your email'>
|
||||||
|
<Field type='email' name='email' />
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Password' help='Please enter your password'>
|
||||||
|
<Field type='password' name='password' />
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Confirm Password' help='Please confirm your password'>
|
||||||
|
<Field type='password' name='confirm' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton
|
||||||
|
type='submit'
|
||||||
|
label={loading ? 'Loading...' : 'Register' }
|
||||||
|
color='info'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
href={'/login'}
|
||||||
|
label={'Login'}
|
||||||
|
color='info'
|
||||||
|
/>
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</SectionFullScreen>
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Register.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
|
};
|
||||||
@ -1,83 +1,62 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import CardBox from '../components/CardBox';
|
||||||
import axios from 'axios';
|
|
||||||
import { mdiCheckCircleOutline, mdiAlertCircleOutline } from '@mdi/js';
|
|
||||||
import BaseIcon from '../components/BaseIcon';
|
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
import SectionFullScreen from '../components/SectionFullScreen';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export default function VerifyEmail() {
|
export default function Verify() {
|
||||||
const router = useRouter();
|
const [loading, setLoading] = React.useState(false);
|
||||||
const { token } = router.query;
|
const router = useRouter();
|
||||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
const { token } = router.query;
|
||||||
|
const notify = (type, msg) => toast(msg, { type });
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (token) {
|
if (!token) {
|
||||||
axios
|
router.push('/login');
|
||||||
.put('/auth/verify-email', { token })
|
return;
|
||||||
.then(() => {
|
}
|
||||||
setStatus('success');
|
const handleSubmit = async () => {
|
||||||
setTimeout(() => {
|
|
||||||
router.push('/admin-login');
|
|
||||||
}, 3000);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setStatus('error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [token, router]);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
setLoading(true);
|
||||||
switch (status) {
|
await axios.put('/auth/verify-email', {
|
||||||
case 'loading':
|
token,
|
||||||
return (
|
}).then(verified => {
|
||||||
<div className="text-center">
|
if (verified) {
|
||||||
<h1 className="text-2xl font-semibold mb-4 text-white">Verifying your email...</h1>
|
setLoading(false);
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto"></div>
|
notify('success', 'Your email was verified');
|
||||||
</div>
|
}
|
||||||
);
|
}).catch(error => {
|
||||||
case 'success':
|
setLoading(false);
|
||||||
return (
|
console.log('error: ', error);
|
||||||
<div className="text-center">
|
notify('error', error.response);
|
||||||
<BaseIcon path={mdiCheckCircleOutline} size={64} className="text-emerald-500 mb-4 mx-auto" />
|
}).finally(async () => {
|
||||||
<h1 className="text-2xl font-semibold mb-2 text-white">Email Verified!</h1>
|
await router.push('/login');
|
||||||
<p className="text-slate-400">Your email has been successfully verified. Redirecting you to login...</p>
|
});
|
||||||
</div>
|
};
|
||||||
);
|
handleSubmit().then();
|
||||||
case 'error':
|
}, [token]);
|
||||||
return (
|
|
||||||
<div className="text-center">
|
|
||||||
<BaseIcon path={mdiAlertCircleOutline} size={64} className="text-rose-500 mb-4 mx-auto" />
|
|
||||||
<h1 className="text-2xl font-semibold mb-2 text-white">Verification Failed</h1>
|
|
||||||
<p className="text-slate-400">The verification link is invalid or has expired.</p>
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/admin-login')}
|
|
||||||
className="mt-6 px-6 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors"
|
|
||||||
>
|
|
||||||
Go to Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Verify Email')}</title>
|
<title>{getPageTitle('Verify Email')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionFullScreen bg="violet">
|
<SectionFullScreen bg='violet'>
|
||||||
<div className="w-full max-w-md p-8 bg-slate-900/50 backdrop-blur-xl border border-white/10 rounded-3xl shadow-2xl">
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
{renderContent()}
|
<p>{loading ? 'Loading...' : ''}</p>
|
||||||
</div>
|
</CardBox>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
</>
|
|
||||||
);
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
VerifyEmail.getLayout = function getLayout(page: ReactElement) {
|
Verify.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -81,22 +81,6 @@ export const create = createAsyncThunk('ai_game_projects/createAi_game_projects'
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const generate = createAsyncThunk('ai_game_projects/generate', async (data: any, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const result = await axios.post(
|
|
||||||
'ai_game_projects/generate',
|
|
||||||
{ data }
|
|
||||||
)
|
|
||||||
return result.data
|
|
||||||
} catch (error) {
|
|
||||||
if (!error.response) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rejectWithValue(error.response.data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const uploadCsv = createAsyncThunk(
|
export const uploadCsv = createAsyncThunk(
|
||||||
'ai_game_projects/uploadCsv',
|
'ai_game_projects/uploadCsv',
|
||||||
async (file: File, { rejectWithValue }) => {
|
async (file: File, { rejectWithValue }) => {
|
||||||
@ -211,20 +195,6 @@ export const ai_game_projectsSlice = createSlice({
|
|||||||
fulfilledNotify(state, `${'Ai_game_projects'.slice(0, -1)} has been created`);
|
fulfilledNotify(state, `${'Ai_game_projects'.slice(0, -1)} has been created`);
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.addCase(generate.pending, (state) => {
|
|
||||||
state.loading = true
|
|
||||||
resetNotify(state);
|
|
||||||
})
|
|
||||||
builder.addCase(generate.rejected, (state, action) => {
|
|
||||||
state.loading = false
|
|
||||||
rejectNotify(state, action);
|
|
||||||
})
|
|
||||||
builder.addCase(generate.fulfilled, (state) => {
|
|
||||||
state.loading = false
|
|
||||||
fulfilledNotify(state, `AI Game Project generation started`);
|
|
||||||
state.refetch = true;
|
|
||||||
})
|
|
||||||
|
|
||||||
builder.addCase(update.pending, (state) => {
|
builder.addCase(update.pending, (state) => {
|
||||||
state.loading = true
|
state.loading = true
|
||||||
resetNotify(state);
|
resetNotify(state);
|
||||||
@ -258,4 +228,4 @@ export const ai_game_projectsSlice = createSlice({
|
|||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setRefetch } = ai_game_projectsSlice.actions
|
export const { setRefetch } = ai_game_projectsSlice.actions
|
||||||
|
|
||||||
export default ai_game_projectsSlice.reducer
|
export default ai_game_projectsSlice.reducer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user