REDROOM
PHP 8.2.30
Path:
Logout
Edit File
Size: 30.77 KB
Close
//home/byroehnu/easepay.easetack.com/controllers/authController.js
Text
Base64
const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { prisma } = require('../config/prisma'); const otpService = require('../services/otpService'); // Validate JWT secret on startup if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { console.error('❌ JWT_SECRET must be at least 32 characters long'); process.exit(1); } // Generate JWT token const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }); }; // Generate OTP const generateOTP = () => { return Math.floor(100000 + Math.random() * 900000).toString(); }; // Send OTP const sendOTP = async (req, res) => { try { const { phone, method = 'sms' } = req.body; if (!phone) { return res.status(400).json({ success: false, message: 'Please provide phone number' }); } // Validate phone number format const phoneValidation = otpService.validatePhoneNumber(phone); if (!phoneValidation.isValid) { return res.status(400).json({ success: false, message: phoneValidation.error }); } const formattedPhone = phoneValidation.formatted; // Check if user exists, if not create a temporary user record const existingUser = await prisma.user.findUnique({ where: { phone: formattedPhone } }); try { // Send OTP via selected method using OTP.dev let sendResult; if (method === 'whatsapp') { sendResult = await otpService.sendWhatsApp(formattedPhone); } else { sendResult = await otpService.sendSMS(formattedPhone); } if (!sendResult.success) { throw new Error(sendResult.message || 'Failed to send OTP'); } // Calculate expiry time const otpExpiresAt = new Date(Date.now() + (process.env.OTP_EXPIRY_MINUTES || 10) * 60 * 1000); if (existingUser) { // Update existing user with verification ID await prisma.user.update({ where: { phone: formattedPhone }, data: { otpVerificationId: sendResult.verificationId, otpExpiresAt: otpExpiresAt } }); } else { // Create new user with verification ID await prisma.user.create({ data: { phone: formattedPhone, email: `temp_${Date.now()}_${formattedPhone.replace(/[^0-9]/g, '')}@easepay.temp`, password: await bcrypt.hash('temporary_password_' + Date.now(), 12), firstName: 'User', lastName: 'Temp', otpVerificationId: sendResult.verificationId, otpExpiresAt: otpExpiresAt } }); } console.log(`✅ OTP sent successfully to ${formattedPhone} via ${method}`); console.log(`⏰ OTP expires at: ${otpExpiresAt}`); console.log(`🆔 Verification ID: ${sendResult.verificationId}`); res.status(200).json({ success: true, message: `OTP sent successfully via ${method}`, phone: formattedPhone, verificationId: sendResult.verificationId, expiresIn: `${process.env.OTP_EXPIRY_MINUTES || 10} minutes`, expiresAt: sendResult.expiresAt || otpExpiresAt }); } catch (smsError) { console.error('❌ OTP sending failed:', smsError.message); res.status(500).json({ success: false, message: 'Failed to send OTP. Please try again.', error: process.env.NODE_ENV === 'development' ? smsError.message : undefined }); } } catch (error) { console.error('Send OTP error:', error); res.status(500).json({ success: false, message: 'Failed to send OTP' }); } }; // Verify OTP const verifyOTP = async (req, res) => { try { const { phone, otp, verificationId } = req.body; console.log(`🔍 === OTP VERIFICATION DEBUG ===`); console.log(`📞 Phone: ${phone}`); console.log(`🔢 Received OTP: "${otp}" (length: ${otp?.length}, type: ${typeof otp})`); console.log(`🆔 Verification ID: ${verificationId}`); if (!phone || !otp) { console.log(`❌ Missing phone or OTP`); return res.status(400).json({ success: false, message: 'Please provide phone number and OTP' }); } // Validate OTP format if (!/^\d{5}$/.test(otp)) { console.log(`❌ Invalid OTP format: "${otp}"`); return res.status(400).json({ success: false, message: 'OTP must be exactly 5 digits' }); } console.log(`🔍 Verifying OTP for ${phone}: ${otp}`); // Find user with matching phone const user = await prisma.user.findUnique({ where: { phone }, select: { id: true, phone: true, otpVerificationId: true, otpExpiresAt: true, phoneVerified: true, firstName: true, lastName: true, email: true, role: true } }); if (!user) { console.log(`❌ User not found for phone: ${phone}`); return res.status(404).json({ success: false, message: 'User not found. Please request OTP first.' }); } console.log(`📋 User found:`); console.log(` - Phone: ${user.phone}`); console.log(` - Verification ID: ${user.otpVerificationId}`); console.log(` - OTP Expires: ${user.otpExpiresAt}`); console.log(` - Phone Verified: ${user.phoneVerified}`); // Check if OTP is expired if (!user.otpExpiresAt || new Date() > user.otpExpiresAt) { console.log(`❌ OTP expired. Expires: ${user.otpExpiresAt}, Now: ${new Date()}`); return res.status(400).json({ success: false, message: 'OTP has expired. Please request a new one.' }); } try { let verificationResult = { success: false }; // Fallback: Allow any 5-digit OTP in development if (process.env.NODE_ENV === 'development' && otp && otp.length === 5) { console.log(`🛠️ Development fallback - accepting OTP: ${otp}`); verificationResult = { success: true, message: 'Development mode OTP accepted' }; } else { // Use OTP.dev to verify the OTP verificationResult = await otpService.verifyOTP( verificationId || user.otpVerificationId, otp ); } if (!verificationResult.success) { console.log(`❌ OTP verification failed: ${verificationResult.message}`); return res.status(400).json({ success: false, message: verificationResult.message || 'Invalid OTP' }); } console.log(`✅ OTP verification passed!`); // Mark phone as verified and clear OTP data await prisma.user.update({ where: { id: user.id }, data: { phoneVerified: true, otpVerificationId: null, otpExpiresAt: null } }); // Generate token const token = generateToken(user.id); console.log(`✅ Phone ${phone} verified successfully! Token generated.`); res.status(200).json({ success: true, message: 'Phone verified successfully', token, user: { id: user.id, phone: user.phone, firstName: user.firstName, lastName: user.lastName, email: user.email, role: user.role, phoneVerified: true } }); } catch (verificationError) { console.error('❌ OTP.dev verification error:', verificationError.message); return res.status(400).json({ success: false, message: 'OTP verification failed. Please try again.' }); } } catch (error) { console.error('Verify OTP error:', error); res.status(500).json({ success: false, message: 'Failed to verify OTP' }); } }; // Phone-based login (send OTP for existing users) const phoneLogin = async (req, res) => { try { const { phone } = req.body; if (!phone) { return res.status(400).json({ success: false, message: 'Please provide phone number' }); } // Check if user exists const user = await prisma.user.findUnique({ where: { phone } }); if (!user) { return res.status(404).json({ success: false, message: 'No account found with this phone number. Please register first.' }); } // Generate OTP const otp = generateOTP(); const otpExpiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes // Update user with OTP await prisma.user.update({ where: { phone }, data: { otp, otpExpiresAt } }); // Log OTP to console (in production, send via SMS) console.log(`📱 Login OTP for ${phone}: ${otp}`); console.log(`⏰ OTP expires at: ${otpExpiresAt}`); res.status(200).json({ success: true, message: 'OTP sent for login', phone, // In development, include OTP in response (remove in production) otp: otp }); } catch (error) { console.error('Phone login error:', error); res.status(500).json({ success: false, message: 'Failed to send login OTP' }); } }; const registerUser = async (req, res) => { try { const { email, password, firstName, lastName, phone, businessName } = req.body; // Validation if (!email || !password || !firstName || !lastName) { return res.status(400).json({ success: false, message: 'Please provide all required fields' }); } // Check if user already exists const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { return res.status(400).json({ success: false, message: 'User already exists with this email' }); } // Hash password const salt = await bcrypt.genSalt(12); const hashedPassword = await bcrypt.hash(password, salt); // Create user const newUser = await prisma.user.create({ data: { email, password: hashedPassword, firstName, lastName, phone, businessName }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, businessName: true, createdAt: true } }); // Generate token const token = generateToken(newUser.id); res.status(201).json({ success: true, message: 'User registered successfully', token, user: newUser }); } catch (error) { console.error('Register error:', error); res.status(500).json({ success: false, message: 'Registration failed' }); } }; const loginUser = async (req, res) => { try { const { email, password } = req.body; // Validation if (!email || !password) { return res.status(400).json({ success: false, message: 'Please provide email and password' }); } // Check if user exists const user = await prisma.user.findUnique({ where: { email }, select: { id: true, email: true, password: true, firstName: true, lastName: true, phone: true, businessName: true, isActive: true } }); if (!user) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Check if user is active if (!user.isActive) { return res.status(401).json({ success: false, message: 'Account has been deactivated' }); } // Check password const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Generate token const token = generateToken(user.id); // Remove password from response const { password: _, ...userWithoutPassword } = user; res.status(200).json({ success: true, message: 'Login successful', token, user: userWithoutPassword }); } catch (error) { console.error('Login error:', error); res.status(500).json({ success: false, message: 'Login failed' }); } }; const getMe = async (req, res) => { try { const user = await prisma.user.findUnique({ where: { id: req.user.id }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, businessName: true, isActive: true, emailVerified: true, createdAt: true } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found' }); } res.status(200).json({ success: true, user }); } catch (error) { console.error('Get me error:', error); res.status(500).json({ success: false, message: 'Failed to get user data' }); } }; const getInvitationDetails = async (req, res) => { try { const { token } = req.params; const invitation = await prisma.staffInvitation.findUnique({ where: { token }, include: { business: { select: { name: true } } } }); if (!invitation) { return res.status(404).json({ success: false, message: 'Invitation not found' }); } if (invitation.status !== 'PENDING') { return res.status(400).json({ success: false, message: `Invitation has already been ${invitation.status.toLowerCase()}` }); } if (new Date() > invitation.expiresAt) { return res.status(400).json({ success: false, message: 'Invitation has expired' }); } res.status(200).json({ success: true, data: { businessName: invitation.business.name, role: invitation.role, expiresAt: invitation.expiresAt } }); } catch (error) { console.error('Get invitation details error:', error); res.status(500).json({ success: false, message: 'Failed to fetch invitation details' }); } }; const acceptStaffInvitation = async (req, res) => { try { const { token } = req.body; const userId = req.user.id; const invitation = await prisma.staffInvitation.findUnique({ where: { token } }); if (!invitation || invitation.status !== 'PENDING' || new Date() > invitation.expiresAt) { return res.status(400).json({ success: false, message: 'Invalid or expired invitation' }); } // Check if user is already staff const existingStaff = await prisma.businessStaff.findUnique({ where: { businessId_userId: { businessId: invitation.businessId, userId } } }); if (existingStaff) { return res.status(400).json({ success: false, message: 'You are already a staff member of this business' }); } // Link user to business await prisma.$transaction([ prisma.businessStaff.create({ data: { businessId: invitation.businessId, userId: userId, role: invitation.role, permissions: invitation.permissions } }), prisma.staffInvitation.update({ where: { id: invitation.id }, data: { status: 'ACCEPTED', acceptedById: userId } }), prisma.business.update({ where: { id: invitation.businessId }, data: { hasStaff: true } }) ]); res.status(200).json({ success: true, message: 'Invitation accepted successfully' }); } catch (error) { console.error('Accept invitation error:', error); res.status(500).json({ success: false, message: 'Failed to accept invitation' }); } }; // Setup Password (for settings) const setupPassword = async (req, res) => { try { const { password, confirmPassword } = req.body; const userId = req.user.id; // Validation if (!password || !confirmPassword) { return res.status(400).json({ success: false, message: 'Password and confirmation are required' }); } if (password !== confirmPassword) { return res.status(400).json({ success: false, message: 'Passwords do not match' }); } // Password strength validation if (password.length < 6) { return res.status(400).json({ success: false, message: 'Password must be at least 6 characters long' }); } // Check if user exists const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found' }); } // Hash the password const salt = await bcrypt.genSalt(12); const hashedPassword = await bcrypt.hash(password, salt); // Update user with password await prisma.user.update({ where: { id: userId }, data: { password: hashedPassword, updatedAt: new Date() } }); res.status(200).json({ success: true, message: 'Password setup successful. You can now sign in with your email/phone and password.' }); } catch (error) { console.error('Setup password error:', error); res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' }); } }; // Sign In with Email/Phone and Password const signIn = async (req, res) => { try { const { identifier, password } = req.body; // Validation if (!identifier || !password) { return res.status(400).json({ success: false, message: 'Email/phone and password are required' }); } // Find user by email or phone const user = await prisma.user.findFirst({ where: { OR: [ { email: identifier }, { phone: identifier } ], isActive: true } }); if (!user) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Check if user has set up a password if (!user.password) { return res.status(400).json({ success: false, message: 'Password not set. Please set up your password in settings first.' }); } // Verify password const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { // Increment login attempts await prisma.user.update({ where: { id: user.id }, data: { loginAttempts: user.loginAttempts + 1, ...(user.loginAttempts >= 4 && { lockedUntil: new Date(Date.now() + 15 * 60 * 1000) // Lock for 15 minutes }) } }); return res.status(401).json({ success: false, message: 'Invalid credentials', ...(user.loginAttempts >= 4 && { lockout: 'Account temporarily locked due to multiple failed attempts' }) }); } // Check if account is locked if (user.lockedUntil && user.lockedUntil > new Date()) { return res.status(423).json({ success: false, message: 'Account is temporarily locked. Please try again later.' }); } // Generate session token const sessionToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' }); // Update user login info await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date(), sessionToken, sessionExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days loginAttempts: 0, // Reset login attempts on successful login lockedUntil: null // Remove any lockout } }); // Generate JWT token const token = generateToken(user.id); res.status(200).json({ success: true, message: 'Sign in successful', token, user: { id: user.id, email: user.email, phone: user.phone, firstName: user.firstName, lastName: user.lastName, role: user.role, emailVerified: user.emailVerified, phoneVerified: user.phoneVerified, biometricEnabled: user.biometricEnabled, lastLoginAt: new Date() } }); } catch (error) { console.error('Sign in error:', error); res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' }); } }; // Check Password Status const getPasswordStatus = async (req, res) => { try { const userId = req.user.id; const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, phone: true, password: true } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found' }); } res.status(200).json({ success: true, data: { hasPassword: !!user.password, canSignIn: !!user.password, email: user.email, phone: user.phone } }); } catch (error) { console.error('Get password status error:', error); res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' }); } }; // Change Password (for users who already have a password) const changePassword = async (req, res) => { try { const { currentPassword, newPassword, confirmNewPassword } = req.body; const userId = req.user.id; // Validation if (!currentPassword || !newPassword || !confirmNewPassword) { return res.status(400).json({ success: false, message: 'Current password, new password, and confirmation are required' }); } if (newPassword !== confirmNewPassword) { return res.status(400).json({ success: false, message: 'New passwords do not match' }); } if (newPassword.length < 6) { return res.status(400).json({ success: false, message: 'New password must be at least 6 characters long' }); } // Get user const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user || !user.password) { return res.status(400).json({ success: false, message: 'No current password found. Please set up a password first.' }); } // Verify current password const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password); if (!isCurrentPasswordValid) { return res.status(401).json({ success: false, message: 'Current password is incorrect' }); } // Hash new password const salt = await bcrypt.genSalt(12); const hashedNewPassword = await bcrypt.hash(newPassword, salt); // Update password await prisma.user.update({ where: { id: userId }, data: { password: hashedNewPassword, updatedAt: new Date() } }); res.status(200).json({ success: true, message: 'Password changed successfully' }); } catch (error) { console.error('Change password error:', error); res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' }); } }; // Check user status by phone (for frontend auth flow) const checkUserStatus = async (req, res) => { try { const { phone } = req.body; if (!phone) { return res.status(400).json({ success: false, message: 'Phone number is required' }); } // Validate phone number format const phoneValidation = otpService.validatePhoneNumber(phone); if (!phoneValidation.isValid) { return res.status(400).json({ success: false, message: phoneValidation.error }); } const formattedPhone = phoneValidation.formatted; // Check if user exists const user = await prisma.user.findUnique({ where: { phone: formattedPhone }, select: { id: true, phone: true, phoneVerified: true, password: true, firstName: true, lastName: true, email: true, isActive: true } }); if (!user) { // New user - needs to go through OTP verification return res.json({ success: true, userStatus: 'new', requiresOTP: true, requiresPasswordSetup: false, canSignIn: false, message: 'New user - phone verification required' }); } if (!user.phoneVerified) { // Existing user but phone not verified return res.json({ success: true, userStatus: 'unverified', requiresOTP: true, requiresPasswordSetup: false, canSignIn: false, message: 'Phone verification required' }); } const hasPassword = user.password && user.password !== null; if (!hasPassword) { // Phone verified but no password set return res.json({ success: true, userStatus: 'verified_no_password', requiresOTP: false, requiresPasswordSetup: true, canSignIn: false, message: 'Password setup required' }); } // User exists, phone verified, password set - can sign in directly return res.json({ success: true, userStatus: 'complete', requiresOTP: false, requiresPasswordSetup: false, canSignIn: true, message: 'User can sign in with password', user: { firstName: user.firstName, lastName: user.lastName, phone: user.phone } }); } catch (error) { console.error('Check user status error:', error); res.status(500).json({ success: false, message: 'Failed to check user status' }); } }; // Complete Onboarding (After OTP Verification) const completeOnboarding = async (req, res) => { try { const { phone, businessName, businessAddress, taxId, bnNumber, rcNumber, businessType, receiveMoneyMethods, collectsVat, hasStaff, pin, password } = req.body; if (!phone || !businessName || !businessType) { return res.status(400).json({ success: false, message: 'Phone, Business Name, and Business Type are required' }); } // Find the user created during OTP step const user = await prisma.user.findUnique({ where: { phone } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found. Please verify OTP first.' }); } // Hash PIN and Password if provided let updateData = { businessName, isActive: true }; if (pin) { updateData.pin = await bcrypt.hash(pin, 10); updateData.pinSetAt = new Date(); } if (password) { updateData.password = await bcrypt.hash(password, 12); } // Update User const updatedUser = await prisma.user.update({ where: { phone }, data: updateData }); // Create Business and Business Profile const business = await prisma.business.create({ data: { name: businessName, type: businessType, ownerId: updatedUser.id, vatRegistered: collectsVat || false, hasStaff: hasStaff || false, profile: { create: { address: businessAddress, tinNumber: taxId, bnNumber: bnNumber, rcNumber: rcNumber, receiveMoneyMethods: receiveMoneyMethods || ["CASH", "TRANSFER", "POS"] } } } }); // Create default User Settings (using upsert to avoid duplicate errors if retried) await prisma.userSettings.upsert({ where: { userId: updatedUser.id }, update: {}, create: { userId: updatedUser.id } }); // Generate token const token = generateToken(updatedUser.id); res.status(200).json({ success: true, message: 'Account created successfully', token, user: { id: updatedUser.id, phone: updatedUser.phone, businessName: updatedUser.businessName, businessId: business.id } }); } catch (error) { console.error('Complete onboarding error:', error); res.status(500).json({ success: false, message: 'Failed to complete onboarding', error: error.message }); } }; // Delete Account const deleteAccount = async (req, res) => { try { const userId = req.user.id; // Delete user (Cascade will handle related records if set up, otherwise manual deletion needed) // Assuming Prisma schema has onDelete: Cascade for most relations await prisma.user.delete({ where: { id: userId } }); res.status(200).json({ success: true, message: 'Account deleted successfully' }); } catch (error) { console.error('Delete account error:', error); res.status(500).json({ success: false, message: 'Failed to delete account' }); } }; module.exports = { registerUser, loginUser, getMe, getInvitationDetails, acceptStaffInvitation, sendOTP, verifyOTP, phoneLogin, setupPassword, signIn, getPasswordStatus, changePassword, checkUserStatus, completeOnboarding, deleteAccount };
Save
Close
Exit & Reset
Text mode: syntax highlighting auto-detects file type.
Directory Contents
Dirs: 1 × Files: 17
Delete Selected
Select All
Select None
Sort:
Name
Size
Modified
Enable drag-to-move
Name
Size
Perms
Modified
Actions
controllers
DIR
-
drwxr-xr-x
2026-03-20 23:25:48
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
.htaccess
127 B
lr--r--r--
2026-03-14 01:49:22
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
advancedAuthController.js
15.34 KB
lrw-r--r--
2026-02-19 22:06:47
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
authController.js
30.77 KB
lrw-r--r--
2026-03-04 06:33:18
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
authController_copy.js
62.98 KB
lrw-r--r--
2026-03-01 23:46:52
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
businessController.js
10.79 KB
lrw-r--r--
2026-02-07 16:28:16
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
businessProfileController.js
14.17 KB
lrw-r--r--
2026-02-27 04:02:38
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
creditScoreController.js
6.32 KB
lrw-r--r--
2026-02-06 17:10:19
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
dashboardController.js
15.13 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
expenseController.js
8.50 KB
lrw-r--r--
2026-03-01 05:05:52
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
inventoryController.js
12.15 KB
lrw-r--r--
2026-02-27 04:02:38
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
invoiceController.js
25.39 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
reportController.js
17.61 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
saleController.js
6.45 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
settingsController.js
2.33 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
subscriptionController.js
12.78 KB
lrw-r--r--
2026-02-21 08:13:30
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
taxController.js
9.96 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
webhookController.js
8.51 KB
lrw-r--r--
2026-02-20 19:31:28
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
Zip Selected
If ZipArchive is unavailable, a
.tar
will be created (no compression).