UNPKG

@codehance/rapid-stack

Version:

A modern full-stack development toolkit for rapid application development

948 lines (831 loc) 32.9 kB
const Generator = require('yeoman-generator'); const path = require('path'); const fs = require('fs'); const { handlePrompt } = require('../../lib/utils'); class FrontendAuthGenerator extends Generator { constructor(args, opts) { super(args, opts); // Add force option this.option('force', { type: Boolean, description: 'Force overwrite files without confirmation', default: true }); // Fixed values for folder name and destination path this.backendMutationsPath = 'backend/app/graphql/mutations/user_mutations'; this.frontendMutationsPath = 'frontend/src/app/graphql/mutations/auth'; this.userModelPath = 'backend/app/models/user.rb'; this.frontendPagesPath = 'frontend/src/app/auth/pages'; this.frontendAuthPath = 'frontend/src/app/auth'; // List of mutation files to create this.mutations = [ { name: 'signIn', backendFile: 'sign_in.rb', templateFile: 'signIn.mutation.ts.ejs' }, { name: 'createUser', backendFile: 'create_user.rb', templateFile: 'createUser.mutation.ts.ejs' }, { name: 'logout', backendFile: 'logout.rb', templateFile: 'logout.mutation.ts.ejs' }, { name: 'passwordReset', backendFile: 'password_reset.rb', templateFile: 'passwordReset.mutation.ts.ejs' }, { name: 'updatePassword', backendFile: 'update_password.rb', templateFile: 'updatePassword.mutation.ts.ejs' }, { name: 'otpRequest', backendFile: 'otp_request.rb', templateFile: 'otpRequest.mutation.ts.ejs' }, { name: 'updateUser', backendFile: 'update_user.rb', templateFile: 'updateUser.mutation.ts.ejs' }, { name: 'twoFactorSetup', backendFile: 'setup_two_factor.rb', templateFile: 'twoFactorSetup.mutation.ts.ejs' }, { name: 'verifyTwoFactorSetup', backendFile: 'setup_two_factor.rb', templateFile: 'verifyTwoFactorSetup.mutation.ts.ejs' } ]; // List of pages to create this.pages = [ { name: 'signup', mutationName: 'createUser' }, { name: 'login', mutationName: 'signIn' }, { name: 'forgot-password', mutationName: 'otpRequest' }, { name: 'verify-otp', mutationName: 'passwordReset' }, { name: 'update-user', mutationName: 'updateUser' }, { name: 'update-password', mutationName: 'updatePassword' }, { name: 'two-factor-setup', mutationName: 'twoFactorSetup' }, { name: 'two-factor-verify', mutationName: 'verifyTwoFactorSetup' } ]; // Fields to exclude from the response data this.excludedResponseFields = [ 'encrypted_password', 'reset_password_token', 'reset_password_sent_at', 'remember_created_at', 'reset_password_token_expires_at', 'role', 'created_at', 'updated_at', '_id', 'password', 'password_confirmation' ]; } initializing() { this.log('This generator will create frontend GraphQL mutation files for authentication.'); if (this.options.force) { this.log('Force mode enabled - files will be overwritten without confirmation'); } } async prompting() { this.answers = await handlePrompt(this, [ { type: 'confirm', name: 'includeSignIn', message: 'Include sign in mutation?', default: true }, { type: 'confirm', name: 'includeCreateUser', message: 'Include create user mutation?', default: true }, { type: 'confirm', name: 'includeLogout', message: 'Include logout mutation?', default: true }, { type: 'confirm', name: 'includePasswordReset', message: 'Include password reset mutation?', default: true }, { type: 'confirm', name: 'includeUpdatePassword', message: 'Include update password mutation?', default: true }, { type: 'confirm', name: 'includeOtpRequest', message: 'Include OTP request mutation?', default: true }, { type: 'confirm', name: 'includeUpdateUser', message: 'Include update user mutation?', default: true }, { type: 'confirm', name: 'includePages', message: 'Include auth pages?', default: true }, { type: 'confirm', name: 'includeTwoFactorSetup', message: 'Include two-factor setup mutation?', default: true }, { type: 'confirm', name: 'includeVerifyTwoFactorSetup', message: 'Include verify two-factor setup mutation?', default: true } ]); } writing() { // Create the destination directory if it doesn't exist const destPath = path.join(process.cwd(), this.frontendMutationsPath); if (!fs.existsSync(destPath)) { this.log(`Creating directory: ${destPath}`); fs.mkdirSync(destPath, { recursive: true }); } // Create the auth directory if it doesn't exist const authPath = path.join(process.cwd(), this.frontendAuthPath); if (!fs.existsSync(authPath)) { this.log(`Creating directory: ${authPath}`); fs.mkdirSync(authPath, { recursive: true }); } // Note: We don't create the auth services directory anymore since we're using the one from services/auth // Create the auth guards directory if it doesn't exist const authGuardsPath = path.join(process.cwd(), this.frontendAuthPath, 'guards'); if (!fs.existsSync(authGuardsPath)) { this.log(`Creating directory: ${authGuardsPath}`); fs.mkdirSync(authGuardsPath, { recursive: true }); } // Create auth.route.ts if it doesn't exist const authRoutePath = path.join(authPath, 'auth.route.ts'); if (!fs.existsSync(authRoutePath)) { this.log('Creating auth.route.ts'); this.fs.copyTpl( this.templatePath('auth.route.ts.ejs'), authRoutePath, {}, {}, { force: this.options.force } ); } // Create auth.component.ts if it doesn't exist const authComponentPath = path.join(authPath, 'auth.component.ts'); if (!fs.existsSync(authComponentPath)) { this.log('Creating auth.component.ts'); this.fs.copyTpl( this.templatePath('auth/auth.component.ts.ejs'), authComponentPath, {}, {}, { force: this.options.force } ); } // Create auth.guard.ts if it doesn't exist const authGuardPath = path.join(authGuardsPath, 'auth.guard.ts'); if (!fs.existsSync(authGuardPath)) { this.log('Creating auth.guard.ts'); this.fs.copyTpl( this.templatePath('auth/guards/auth.guard.ts.ejs'), authGuardPath, {}, {}, { force: this.options.force } ); } // Get user model fields for response data const responseFields = this._getUserModelFields(); // Generate the selected mutation files this._generateMutationFiles(responseFields); // Generate the auth pages if selected if (this.answers.includePages) { this._generateAuthPages(); } // Update app.routes.ts to include auth route if it doesn't exist this._updateAppRoutes(); // Update auth.route.ts to include update-user and update-password routes if they don't exist this._updateAuthRoutes(); // Update auth.service.ts with real implementation this._updateAuthService(); } _generateMutationFiles(responseFields) { this.mutations.forEach(mutation => { const includeKey = `include${mutation.name.charAt(0).toUpperCase() + mutation.name.slice(1)}`; if (this.answers[includeKey]) { this.log(`Generating ${mutation.name}.mutation.ts`); // Get arguments from backend file const args = this._getArgumentsFromBackendFile(mutation.backendFile); // Generate the frontend mutation file this.fs.copyTpl( this.templatePath(mutation.templateFile), path.join(process.cwd(), this.frontendMutationsPath, `${mutation.name}.mutation.ts`), { mutationName: mutation.name, args: args, responseFields: responseFields, // Special case for signIn which needs token in response includeToken: mutation.name === 'signIn' }, {}, { force: this.options.force } ); } }); } _generateAuthPages() { // Create the pages directory if it doesn't exist const pagesPath = path.join(process.cwd(), this.frontendPagesPath); if (!fs.existsSync(pagesPath)) { this.log(`Creating directory: ${pagesPath}`); fs.mkdirSync(pagesPath, { recursive: true }); } // Generate each page this.pages.forEach(page => { // Check if we should include pages and if the corresponding mutation is included const mutationKey = `include${page.mutationName.charAt(0).toUpperCase() + page.mutationName.slice(1)}`; if (this.answers.includePages && this.answers[mutationKey]) { this.log(`Generating ${page.name} page`); // Create the page directory const pageDir = path.join(pagesPath, page.name); if (!fs.existsSync(pageDir)) { fs.mkdirSync(pageDir, { recursive: true }); } // Get the mutation arguments const mutation = this.mutations.find(m => m.name === page.mutationName); const args = mutation ? this._getArgumentsFromBackendFile(mutation.backendFile) : []; // Generate the page files from templates this._generatePageFiles(page, args); } }); } _generatePageFiles(page, args) { const destDir = path.join(process.cwd(), this.frontendPagesPath, page.name); // Generate TypeScript file this.fs.copyTpl( this.templatePath(`pages/${page.name}/${page.name}.page.ts.ejs`), path.join(destDir, `${page.name}.page.ts`), { args: args, snakeToCamel: this._snakeToCamel.bind(this) }, {}, { force: this.options.force } ); // Generate HTML file this.fs.copyTpl( this.templatePath(`pages/${page.name}/${page.name}.page.html.ejs`), path.join(destDir, `${page.name}.page.html`), { args: args, snakeToCamel: this._snakeToCamel.bind(this), formatFieldName: this._formatFieldName.bind(this) }, {}, { force: this.options.force } ); // Generate SCSS file this.fs.copyTpl( this.templatePath(`pages/${page.name}/${page.name}.page.scss.ejs`), path.join(destDir, `${page.name}.page.scss`), {}, {}, { force: this.options.force } ); } _getArgumentsFromBackendFile(filename) { const filePath = path.join(process.cwd(), this.backendMutationsPath, filename); if (!fs.existsSync(filePath)) { this.log.error(`Backend file not found: ${filePath}`); return []; } const content = fs.readFileSync(filePath, 'utf8'); const args = []; // Extract arguments using regex const argRegex = /argument\s+:(\w+),\s+(\w+),\s+required:\s+(true|false)/g; let match; while ((match = argRegex.exec(content)) !== null) { args.push({ name: match[1], type: this._convertRubyTypeToGraphQL(match[2]), required: match[3] === 'true' }); } return args; } _getUserModelFields() { const userModelPath = path.join(process.cwd(), this.userModelPath); if (!fs.existsSync(userModelPath)) { this.log.error(`User model not found at: ${userModelPath}`); return ['id', 'fullName', 'email', 'telephone', 'role']; // Default fields if model not found } const content = fs.readFileSync(userModelPath, 'utf8'); const fields = ['id']; // Always include id // Extract field definitions using regex const fieldRegex = /field\s+:(\w+),\s+type:\s+(\w+)(?:,\s+default:\s+([^,\n]+))?/g; let match; while ((match = fieldRegex.exec(content)) !== null) { const fieldName = match[1]; // Skip excluded fields if (!this.excludedResponseFields.includes(fieldName)) { // Convert snake_case to camelCase for frontend const camelCaseField = this._snakeToCamel(fieldName); // Add field if not already included if (!fields.includes(camelCaseField)) { fields.push(camelCaseField); } } } return fields; } _convertRubyTypeToGraphQL(rubyType) { const typeMap = { 'String': 'String', 'Integer': 'Int', 'Float': 'Float', 'Boolean': 'Boolean', 'Time': 'String', 'Date': 'String', 'DateTime': 'String', 'Hash': 'JSON', 'Array': 'JSON' }; return typeMap[rubyType] || 'String'; } _snakeToCamel(str) { return str.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase()); } _snakeToPascal(str) { return str .split('_') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); } _formatFieldName(camelCase) { // Convert camelCase to Title Case with spaces return camelCase .replace(/([A-Z])/g, ' $1') .replace(/^./, str => str.toUpperCase()); } _updateAppRoutes() { const appRoutesPath = path.join(process.cwd(), 'frontend/src/app/app.routes.ts'); if (!fs.existsSync(appRoutesPath)) { this.log.error(`app.routes.ts not found at: ${appRoutesPath}`); return; } let content = fs.readFileSync(appRoutesPath, 'utf8'); // Update authGuard import path if (content.includes("import { authGuard } from './platforms/auth/guards/auth.guard';")) { content = content.replace( "import { authGuard } from './platforms/auth/guards/auth.guard';", "import { authGuard } from './auth/guards/auth.guard';" ); fs.writeFileSync(appRoutesPath, content, 'utf8'); this.log('Updated authGuard import path in app.routes.ts'); } // Check if auth route already exists if (!content.includes("path: 'auth'")) { this.log('Adding auth route to app.routes.ts'); // Find the routes array const routesStartMatch = content.match(/export const routes: Routes = \[/); if (routesStartMatch) { const routesStartIndex = routesStartMatch.index + routesStartMatch[0].length; // Auth route to insert with updated path (no platforms) const authRoute = ` { path: 'auth', loadComponent: () => import('./auth/auth.component').then(m => m.AuthComponent), loadChildren: () => import('./auth/auth.route').then(m => m.routes), },`; // Insert the auth route after the opening bracket of the routes array content = content.slice(0, routesStartIndex) + authRoute + content.slice(routesStartIndex); fs.writeFileSync(appRoutesPath, content, 'utf8'); this.log('Auth route added to app.routes.ts'); } else { this.log.error('Could not find routes array in app.routes.ts'); } } else { // If auth route exists but uses old path, update it const oldRoutePath = /loadComponent:\s*\(\)\s*=>\s*import\(['"]\.\/platforms\/auth\/auth\.component['"]\)/; if (oldRoutePath.test(content)) { content = content.replace( oldRoutePath, "loadComponent: () => import('./auth/auth.component')" ); const oldChildrenPath = /loadChildren:\s*\(\)\s*=>\s*import\(['"]\.\/platforms\/auth\/auth\.route['"]\)/; if (oldChildrenPath.test(content)) { content = content.replace( oldChildrenPath, "loadChildren: () => import('./auth/auth.route')" ); fs.writeFileSync(appRoutesPath, content, 'utf8'); this.log('Updated existing auth route paths in app.routes.ts'); } } else { this.log('Auth route already exists in app.routes.ts with correct paths'); } } } _updateAuthRoutes() { const authRoutesPath = path.join(process.cwd(), 'frontend/src/app/auth/auth.route.ts'); if (!fs.existsSync(authRoutesPath)) { this.log.error(`auth.route.ts not found at: ${authRoutesPath}`); return; } let content = fs.readFileSync(authRoutesPath, 'utf8'); let modified = false; // Check if update-user route already exists if (!content.includes("path: 'update-user'")) { this.log('Adding update-user route to auth.route.ts'); // Find the routes array const routesMatch = content.match(/export const routes: Routes = \[/); if (routesMatch) { const routesStartIndex = routesMatch.index + routesMatch[0].length; // Update-user route to insert const updateUserRoute = ` { path: 'update-user', loadComponent: () => import('./pages/update-user/update-user.page').then(m => m.UpdateUserPage), canActivate: [authGuard] },`; // Insert the update-user route at the beginning of the routes array // Check first if this is a newly created file if (content.includes("loadComponent: () => import('./pages/forgot-password/forgot-password.page')")) { // Insert after forgot-password route const forgotPasswordMatch = content.match(/path:\s*'forgot-password'[\s\S]*?\},/); if (forgotPasswordMatch) { const insertionIndex = forgotPasswordMatch.index + forgotPasswordMatch[0].length; content = content.slice(0, insertionIndex) + updateUserRoute + content.slice(insertionIndex); modified = true; } else { content = content.slice(0, routesStartIndex) + updateUserRoute + content.slice(routesStartIndex); modified = true; } } else { content = content.slice(0, routesStartIndex) + updateUserRoute + content.slice(routesStartIndex); modified = true; } } else { this.log.error('Could not find routes array in auth.route.ts'); } } else { this.log('Update-user route already exists in auth.route.ts'); } // Check if update-password route already exists if (!content.includes("path: 'update-password'")) { this.log('Adding update-password route to auth.route.ts'); // Find the routes array const routesMatch = content.match(/export const routes: Routes = \[/); if (routesMatch) { const routesStartIndex = routesMatch.index + routesMatch[0].length; // Update-password route to insert const updatePasswordRoute = ` { path: 'update-password', loadComponent: () => import('./pages/update-password/update-password.page').then(m => m.UpdatePasswordPage), canActivate: [authGuard] },`; // Insert the update-password route // If update-user was just added, we need to insert after it if (content.includes("path: 'update-user'")) { const updateUserMatch = content.match(/path:\s*'update-user'[\s\S]*?\},/); if (updateUserMatch) { const insertionIndex = updateUserMatch.index + updateUserMatch[0].length; content = content.slice(0, insertionIndex) + updatePasswordRoute + content.slice(insertionIndex); modified = true; } else { content = content.slice(0, routesStartIndex) + updatePasswordRoute + content.slice(routesStartIndex); modified = true; } } else { content = content.slice(0, routesStartIndex) + updatePasswordRoute + content.slice(routesStartIndex); modified = true; } } else { this.log.error('Could not find routes array in auth.route.ts'); } } else { this.log('Update-password route already exists in auth.route.ts'); } // Check if login route already exists if (!content.includes("path: 'login'")) { this.log('Adding login route to auth.route.ts'); // Find the routes array const routesMatch = content.match(/export const routes: Routes = \[/); if (routesMatch) { const routesStartIndex = routesMatch.index + routesMatch[0].length; // Login route to insert const loginRoute = ` { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'login', loadComponent: () => import('./pages/login/login.page').then(m => m.LoginPage) },`; // Insert the login route content = content.slice(0, routesStartIndex) + loginRoute + content.slice(routesStartIndex); modified = true; } else { this.log.error('Could not find routes array in auth.route.ts'); } } else { this.log('Login route already exists in auth.route.ts'); } // Check if signup route already exists if (!content.includes("path: 'signup'")) { this.log('Adding signup route to auth.route.ts'); // Find the routes array const routesMatch = content.match(/export const routes: Routes = \[/); if (routesMatch) { const routesStartIndex = routesMatch.index + routesMatch[0].length; // Signup route to insert const signupRoute = ` { path: 'signup', loadComponent: () => import('./pages/signup/signup.page').then(m => m.SignupPage) },`; // Insert the signup route if (content.includes("path: 'login'")) { const loginMatch = content.match(/path:\s*'login'[\s\S]*?\},/); if (loginMatch) { const insertionIndex = loginMatch.index + loginMatch[0].length; content = content.slice(0, insertionIndex) + signupRoute + content.slice(insertionIndex); modified = true; } else { content = content.slice(0, routesStartIndex) + signupRoute + content.slice(routesStartIndex); modified = true; } } else { content = content.slice(0, routesStartIndex) + signupRoute + content.slice(routesStartIndex); modified = true; } } else { this.log.error('Could not find routes array in auth.route.ts'); } } else { this.log('Signup route already exists in auth.route.ts'); } // Check if forgot-password route already exists if (!content.includes("path: 'forgot-password'")) { this.log('Adding forgot-password route to auth.route.ts'); // Find the routes array const routesMatch = content.match(/export const routes: Routes = \[/); if (routesMatch) { const routesStartIndex = routesMatch.index + routesMatch[0].length; // Forgot-password route to insert const forgotPasswordRoute = ` { path: 'forgot-password', loadComponent: () => import('./pages/forgot-password/forgot-password.page').then(m => m.ForgotPasswordPage) },`; // Insert the forgot-password route if (content.includes("path: 'signup'")) { const signupMatch = content.match(/path:\s*'signup'[\s\S]*?\},/); if (signupMatch) { const insertionIndex = signupMatch.index + signupMatch[0].length; content = content.slice(0, insertionIndex) + forgotPasswordRoute + content.slice(insertionIndex); modified = true; } else { content = content.slice(0, routesStartIndex) + forgotPasswordRoute + content.slice(routesStartIndex); modified = true; } } else { content = content.slice(0, routesStartIndex) + forgotPasswordRoute + content.slice(routesStartIndex); modified = true; } } else { this.log.error('Could not find routes array in auth.route.ts'); } } else { this.log('Forgot-password route already exists in auth.route.ts'); } // Check if verify-otp route already exists if (!content.includes("path: 'verify-otp'")) { this.log('Adding verify-otp route to auth.route.ts'); // Find the routes array const routesMatch = content.match(/export const routes: Routes = \[/); if (routesMatch) { const routesStartIndex = routesMatch.index + routesMatch[0].length; // Verify-otp route to insert const verifyOtpRoute = ` { path: 'verify-otp', loadComponent: () => import('./pages/verify-otp/verify-otp.page').then(m => m.VerifyOtpPage) },`; // Insert the verify-otp route - look for forgot-password route first if (content.includes("path: 'forgot-password'")) { const forgotPasswordMatch = content.match(/path:\s*'forgot-password'[\s\S]*?\},/); if (forgotPasswordMatch) { const insertionIndex = forgotPasswordMatch.index + forgotPasswordMatch[0].length; content = content.slice(0, insertionIndex) + verifyOtpRoute + content.slice(insertionIndex); modified = true; } else { // If we can't find forgot-password route match with regex, try a more precise approach const forgotPasswordStr = "path: 'forgot-password'"; const forgotIndex = content.indexOf(forgotPasswordStr); if (forgotIndex !== -1) { // Find the closing brace of this route const routeClosingIndex = content.indexOf("}", forgotIndex); if (routeClosingIndex !== -1) { // Check if there's already a comma after the closing brace const afterClosingBrace = content.substring(routeClosingIndex + 1, routeClosingIndex + 5).trim(); // If there's no comma, add one if (!afterClosingBrace.startsWith(',')) { // Insert after the closing brace with a comma content = content.slice(0, routeClosingIndex + 1) + "," + verifyOtpRoute + content.slice(routeClosingIndex + 1); } else { // Comma already exists, just insert after it content = content.slice(0, routeClosingIndex + 2) + verifyOtpRoute + content.slice(routeClosingIndex + 2); } modified = true; } else { // Fallback: Add it just before the closing bracket of the routes array const closingBracketIndex = content.lastIndexOf('];'); if (closingBracketIndex !== -1) { content = content.slice(0, closingBracketIndex) + verifyOtpRoute + content.slice(closingBracketIndex); modified = true; } } } else { // Fallback: Add it just before the closing bracket of the routes array const closingBracketIndex = content.lastIndexOf('];'); if (closingBracketIndex !== -1) { content = content.slice(0, closingBracketIndex) + verifyOtpRoute + content.slice(closingBracketIndex); modified = true; } } } } else { // If forgot-password route doesn't exist, add after whatever route is last // Try to find the last route in the array const closingBracketIndex = content.lastIndexOf('];'); if (closingBracketIndex !== -1) { // Check if there's at least one route already in the array const lastRouteMatch = content.substring(0, closingBracketIndex).match(/\{\s*path:\s*['"].*?['"]\s*,[\s\S]*?\},\s*$/); if (lastRouteMatch) { const insertionIndex = lastRouteMatch.index + lastRouteMatch[0].length; content = content.slice(0, insertionIndex) + verifyOtpRoute + content.slice(insertionIndex); modified = true; } else { // If no route exists, just add before the closing bracket content = content.slice(0, closingBracketIndex) + verifyOtpRoute + content.slice(closingBracketIndex); modified = true; } } else { this.log.error('Could not find a suitable insertion point for verify-otp route in auth.route.ts'); } } } else { this.log.error('Could not find routes array in auth.route.ts'); } } else { this.log('Verify-otp route already exists in auth.route.ts'); } // Write the updated content back to the file if modified if (modified) { // Check if authGuard is imported if (!content.includes('import { authGuard }')) { // Add import for authGuard if not already present // Check if the import is from 'src/app/auth/guards/auth.guard' or '../guards/auth.guard' if (content.includes("from 'src/app/auth/guards/auth.guard'")) { // Import already exists with full path } else if (content.includes("from '../guards/auth.guard'")) { // Import already exists with relative path } else { // Add the import with the same path format as existing imports if (content.includes("from 'src/app")) { // Use full path format const importStatement = "import { authGuard } from 'src/app/auth/guards/auth.guard';\n"; content = importStatement + content; } else { // Use relative path format const importStatement = "import { authGuard } from '../guards/auth.guard';\n"; content = importStatement + content; } } } fs.writeFileSync(authRoutesPath, content, 'utf8'); this.log('Auth routes updated successfully'); } } _updateAuthService() { const authServicePath = path.join(process.cwd(), 'frontend/src/app/services/auth/auth.service.ts'); if (!fs.existsSync(authServicePath)) { this.log.error('auth.service.ts not found'); return; } try { let content = fs.readFileSync(authServicePath, 'utf8'); // Uncomment the SignInMutation import content = content.replace( /\/\/ import \{ SignInMutation \} from/, 'import { SignInMutation } from' ); // First find the signIn method const signInMethodStart = ' signIn(input: any): Observable<any> {'; const nextMethodStart = ' storeUserData(result: any): Observable<void> {'; const startIndex = content.indexOf(signInMethodStart); if (startIndex === -1) { this.log.error('Could not find signIn method in auth.service.ts'); return; } const endIndex = content.indexOf(nextMethodStart, startIndex); if (endIndex === -1) { this.log.error('Could not find end of signIn method in auth.service.ts'); return; } // Extract parts of the file const beforeMethod = content.substring(0, startIndex); const afterMethod = content.substring(endIndex); // Create the real implementation const realImplementation = ` signIn(input: any): Observable<any> { // Real implementation added by frontend-auth generator // First logout to clear any stale data return this.logout().pipe( switchMap(() => { return this.apollo .mutate({ mutation: SignInMutation, variables: { email: input.email, password: input.password } }) .pipe( map((response: any) => { const result = response.data.signIn; if (result.errors && result.errors.length > 0) { throw this.errorService.validationError(result.errors); } return result; }), switchMap(result => this.storeUserData(result)) ); }) ); } `; // Combine the parts const newContent = beforeMethod + realImplementation + afterMethod; // Write the file back fs.writeFileSync(authServicePath, newContent, 'utf8'); this.log('Successfully updated auth.service.ts with real implementation'); } catch (error) { this.log.error(`Error updating auth.service.ts: ${error.message}`); } } end() { this.log('Frontend authentication files have been created successfully.'); } } module.exports = FrontendAuthGenerator;