diff --git a/backend/app/Api/User/Session.php b/backend/app/Api/User/Session.php index 3dbbf99d..fcd3120e 100755 --- a/backend/app/Api/User/Session.php +++ b/backend/app/Api/User/Session.php @@ -30,12 +30,13 @@ */ use MythicalClient\App; +use MythicalClient\Chat\Mails; use MythicalClient\Chat\User; use MythicalClient\Chat\Roles; use MythicalClient\Chat\Billing; use MythicalClient\Chat\Session; -use MythicalClient\Chat\columns\UserColumns; use MythicalClient\Chat\UserActivities; +use MythicalClient\Chat\columns\UserColumns; $router->post('/api/user/session/info/update', function (): void { App::init(); @@ -183,7 +184,6 @@ }); - $router->get('/api/user/session/activities', function (): void { App::init(); $appInstance = App::getInstance(true); @@ -196,6 +196,8 @@ $accountToken = $session->SESSION_KEY; $appInstance->OK('User activities', [ - 'activities' => UserActivities::get(User::getInfo($accountToken, UserColumns::UUID, false)) + 'activities' => UserActivities::get(User::getInfo($accountToken, UserColumns::UUID, false)), ]); -}); \ No newline at end of file +}); + + diff --git a/backend/app/Api/User/Session/Emails.php b/backend/app/Api/User/Session/Emails.php new file mode 100755 index 00000000..9f4ca454 --- /dev/null +++ b/backend/app/Api/User/Session/Emails.php @@ -0,0 +1,89 @@ +<?php +use MythicalClient\App; +use MythicalClient\Chat\Mails; +use MythicalClient\Chat\User; +use MythicalClient\Chat\Roles; +use MythicalClient\Chat\Billing; +use MythicalClient\Chat\Session; +use MythicalClient\Chat\UserActivities; +use MythicalClient\Chat\columns\UserColumns; + + +$router->get('/api/user/session/emails', function (): void { + App::init(); + $appInstance = App::getInstance(true); + $config = $appInstance->getConfig(); + + $appInstance->allowOnlyGET(); + + $session = new Session($appInstance); + + $accountToken = $session->SESSION_KEY; + + $appInstance->OK('User emails', [ + 'emails' => Mails::getAll(User::getInfo($accountToken, UserColumns::UUID, false)) + ]); +}); + + +$router->get('/api/user/session/emails/(.*)/raw', function (string $id): void { + $appInstance = App::getInstance(true); + $config = $appInstance->getConfig(); + if ($id == '') { + die(header('location: /account')); + } + + if (!is_numeric($id)) { + die(header('location: /account')); + } + $id = (int) $id; + + $appInstance->allowOnlyGET(); + + $session = new Session($appInstance); + + $accountToken = $session->SESSION_KEY; + + if (Mails::exists($id)) { + if (Mails::doesUserOwnEmail(User::getInfo($accountToken, UserColumns::UUID, false), $id)) { + $mail = Mails::get($id); + header('Content-Type: text/html; charset=utf-8'); + echo $mail['body']; + exit; + } else { + die(header('location: /account')); + } + } else { + die(header('location: /account')); + } +}); + +$router->delete('/api/user/session/emails/(.*)/delete', function (string $id): void { + $appInstance = App::getInstance(true); + $config = $appInstance->getConfig(); + if ($id == '') { + $appInstance->BadRequest('Email not found!', ['error_code' => 'EMAIL_NOT_FOUND']); + } + + if (!is_numeric($id)) { + $appInstance->BadRequest('Email not found!', ['error_code' => 'EMAIL_NOT_FOUND']); + } + $id = (int) $id; + + $appInstance->allowOnlyDELETE(); + + $session = new Session($appInstance); + + $accountToken = $session->SESSION_KEY; + + if (Mails::exists($id)) { + if (Mails::doesUserOwnEmail(User::getInfo($accountToken, UserColumns::UUID, false), $id)) { + Mails::delete($id, User::getInfo($accountToken, UserColumns::UUID, false)); + $appInstance->OK('Email deleted successfully!', []); + } else { + $appInstance->Unauthorized('Unauthorized', ['error_code' => 'UNAUTHORIZED']); + } + } else { + $appInstance->BadRequest('Email not found!', ['error_code' => 'EMAIL_NOT_FOUND']); + } +}); diff --git a/backend/app/Chat/Mails.php b/backend/app/Chat/Mails.php new file mode 100755 index 00000000..17c1965a --- /dev/null +++ b/backend/app/Chat/Mails.php @@ -0,0 +1,169 @@ +<?php + +/* + * This file is part of MythicalClient. + * Please view the LICENSE file that was distributed with this source code. + * + * MIT License + * + * (c) MythicalSystems <mythicalsystems.xyz> - All rights reserved + * (c) NaysKutzu <nayskutzu.xyz> - All rights reserved + * (c) Cassian Gherman <nayskutzu.xyz> - All rights reserved + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace MythicalClient\Chat; + +class Mails +{ + /** + * Add a mail. + * + * @param string $subject Mail subject + * @param string $body Mail body + * @param string $uuid User UUID + * + * @return bool + */ + public static function add(string $subject, string $body, string $uuid): bool + { + try { + $dbConn = Database::getPdoConnection(); + $from = \MythicalClient\App::getInstance(true)->getConfig()->getSetting(\MythicalClient\Config\ConfigInterface::SMTP_FROM, 'system@mythical.systems'); + $stmt = $dbConn->prepare('INSERT INTO ' . self::getTableName() . ' (subject, body, `from`, `user`) VALUES (:subject, :body, :from, :user)'); + $stmt->bindParam(':subject', $subject); + $stmt->bindParam(':body', $body); + $stmt->bindParam(':from', $from); + $stmt->bindParam(':user', $uuid); + + return $stmt->execute(); + } catch (\Exception $e) { + return false; + } + + } + /** + * Delete a mail. + * + * @param string $id Mail ID + * @param string $uuid User UUID + * + * @return bool + */ + public static function delete(string $id, string $uuid): bool + { + try { + $dbConn = Database::getPdoConnection(); + $stmt = $dbConn->prepare('DELETE FROM ' . self::getTableName() . ' WHERE id = :id AND `user` = :user'); + $stmt->bindParam(':id', $id); + $stmt->bindParam(':user', $uuid); + + return $stmt->execute(); + } catch (\Exception $e) { + return false; + } + } + /** + * Get all mails for a user. + * + * @param string $uuid User UUID + * + * @return array + */ + public static function getAll(string $uuid): array + { + try { + $dbConn = Database::getPdoConnection(); + $stmt = $dbConn->prepare('SELECT * FROM ' . self::getTableName() . ' WHERE `user` = :user ORDER BY id DESC LIMIT 50'); + $stmt->bindParam(':user', $uuid); + $stmt->execute(); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } catch (\Exception $e) { + return []; + } + } + /** + * Get a mail. + * + * @param string $id Mail ID + * + * @return array Mail data + */ + public static function get(string $id): array + { + try { + $dbConn = Database::getPdoConnection(); + $stmt = $dbConn->prepare('SELECT * FROM ' . self::getTableName() . ' WHERE id = :id'); + $stmt->bindParam(':id', $id); + $stmt->execute(); + + return $stmt->fetch(\PDO::FETCH_ASSOC); + } catch (\Exception $e) { + return []; + } + } + /** + * Check if a mail exists. + * + * @param string $id Mail ID + * + * @return bool Does mail exist + */ + public static function exists(string $id): bool + { + try { + $dbConn = Database::getPdoConnection(); + $stmt = $dbConn->prepare('SELECT * FROM ' . self::getTableName() . ' WHERE id = :id'); + $stmt->bindParam(':id', $id); + $stmt->execute(); + return $stmt->rowCount() > 0; + } catch (\Exception $e) { + return false; + } + } + /** + * Get all mails for a user. + * + * @param string $uuid User UUID + * @param string $id Mail ID + * + * @return bool Does user own email + */ + public static function doesUserOwnEmail(string $uuid, string $id): bool + { + try { + $dbConn = Database::getPdoConnection(); + $stmt = $dbConn->prepare('SELECT * FROM ' . self::getTableName() . ' WHERE id = :id AND `user` = :user'); + $stmt->bindParam(':id', $id); + $stmt->bindParam(':user', $uuid); + $stmt->execute(); + + return $stmt->rowCount() > 0; + } catch (\Exception $e) { + return false; + } + } + + public static function getTableName(): string + { + return 'mythicalclient_users_mails'; + } +} diff --git a/backend/app/Chat/User.php b/backend/app/Chat/User.php index ec4086d3..79655ce4 100755 --- a/backend/app/Chat/User.php +++ b/backend/app/Chat/User.php @@ -33,14 +33,14 @@ use Gravatar\Gravatar; use MythicalClient\App; -use MythicalClient\Chat\interface\UserActivitiesTypes; use MythicalClient\Mail\Mail; use MythicalClient\Mail\templates\Verify; +use MythicalSystems\CloudFlare\CloudFlare; use MythicalClient\Mail\templates\NewLogin; use MythicalClient\Chat\columns\UserColumns; use MythicalClient\Mail\templates\ResetPassword; +use MythicalClient\Chat\interface\UserActivitiesTypes; use MythicalClient\Chat\columns\EmailVerificationColumns; -use MythicalSystems\CloudFlare\CloudFlare; class User extends Database { @@ -225,6 +225,7 @@ public static function login(string $login, string $password): string } } UserActivities::add($user['uuid'], UserActivitiesTypes::$login, CloudFlare::getRealUserIP()); + return $user['token']; } diff --git a/backend/app/Chat/UserActivities.php b/backend/app/Chat/UserActivities.php index 4f28e061..af077fe1 100755 --- a/backend/app/Chat/UserActivities.php +++ b/backend/app/Chat/UserActivities.php @@ -1,52 +1,84 @@ <?php + +/* + * This file is part of MythicalClient. + * Please view the LICENSE file that was distributed with this source code. + * + * MIT License + * + * (c) MythicalSystems <mythicalsystems.xyz> - All rights reserved + * (c) NaysKutzu <nayskutzu.xyz> - All rights reserved + * (c) Cassian Gherman <nayskutzu.xyz> - All rights reserved + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + namespace MythicalClient\Chat; use MythicalClient\Chat\interface\UserActivitiesTypes; -class UserActivities { - +class UserActivities +{ /** - * Add user activity - * + * Add user activity. + * * @param string $uuid User UUID - * @param string|\MythicalClient\Chat\interface\UserActivitiesTypes $type Activity type + * @param string|UserActivitiesTypes $type Activity type * @param string $ipv4 IP address - * - * @return bool */ - public static function add(string $uuid, string|UserActivitiesTypes $type, string $ipv4) : bool { + public static function add(string $uuid, string|UserActivitiesTypes $type, string $ipv4): bool + { $dbConn = Database::getPdoConnection(); - - $stmt = $dbConn->prepare("INSERT INTO " . self::getTable() . " (user, action, ip_address) VALUES (:user, :action, :ip_address)"); + + $stmt = $dbConn->prepare('INSERT INTO ' . self::getTable() . ' (user, action, ip_address) VALUES (:user, :action, :ip_address)'); + return $stmt->execute([ ':user' => $uuid, ':action' => $type, - ':ip_address' => $ipv4 + ':ip_address' => $ipv4, ]); } + /** - * Get user activities - * + * Get user activities. + * * @param string $uuid User UUID - * - * @return array */ - public static function get(string $uuid) : array { + public static function get(string $uuid): array + { $dbConn = Database::getPdoConnection(); - - $stmt = $dbConn->prepare("SELECT * FROM " . self::getTable() . " WHERE user = :user LIMIT 125"); + + $stmt = $dbConn->prepare('SELECT * FROM ' . self::getTable() . ' WHERE user = :user LIMIT 125'); $stmt->execute([ - ':user' => $uuid + ':user' => $uuid, ]); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); } /** - * Get table name - * + * Get table name. + * * @return string Table name */ - public static function getTable() : string { - return "mythicalclient_users_activities"; + public static function getTable(): string + { + return 'mythicalclient_users_activities'; } -} \ No newline at end of file +} diff --git a/backend/app/Chat/interface/UserActivitiesTypes.php b/backend/app/Chat/interface/UserActivitiesTypes.php index c2285712..653c2e61 100755 --- a/backend/app/Chat/interface/UserActivitiesTypes.php +++ b/backend/app/Chat/interface/UserActivitiesTypes.php @@ -1,20 +1,51 @@ <?php +/* + * This file is part of MythicalClient. + * Please view the LICENSE file that was distributed with this source code. + * + * MIT License + * + * (c) MythicalSystems <mythicalsystems.xyz> - All rights reserved + * (c) NaysKutzu <nayskutzu.xyz> - All rights reserved + * (c) Cassian Gherman <nayskutzu.xyz> - All rights reserved + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + namespace MythicalClient\Chat\interface; -class UserActivitiesTypes { - public static string $login = "auth:login"; - public static string $register = "auth:register"; - +class UserActivitiesTypes +{ + public static string $login = 'auth:login'; + public static string $register = 'auth:register'; + /** - * Get all types - * + * Get all types. + * * @return array All types */ - public static function getTypes(): array { + public static function getTypes(): array + { return [ self::$login, - self::$register + self::$register, ]; } -} \ No newline at end of file +} diff --git a/backend/app/Mail/templates/NewLogin.php b/backend/app/Mail/templates/NewLogin.php index 0a14630b..793da498 100755 --- a/backend/app/Mail/templates/NewLogin.php +++ b/backend/app/Mail/templates/NewLogin.php @@ -32,6 +32,7 @@ namespace MythicalClient\Mail\templates; use MythicalClient\App; +use MythicalClient\Chat\Mails; use MythicalClient\Chat\User; use MythicalClient\Mail\Mail; use MythicalClient\Chat\Database; @@ -45,6 +46,7 @@ public static function sendMail(string $uuid): void try { $template = self::getFinalTemplate($uuid); $email = User::getInfo(User::getTokenFromUUID($uuid), UserColumns::EMAIL, false); + Mails::add('New Login Detected', $template, $uuid); self::send($email, 'New Login Detected', $template); } catch (\Exception $e) { App::getInstance(true)->getLogger()->error('(' . APP_SOURCECODE_DIR . '/Mail/templates/NewLogin.php) [sendMail] Failed to send email: ' . $e->getMessage()); diff --git a/backend/app/Mail/templates/ResetPassword.php b/backend/app/Mail/templates/ResetPassword.php index 9f7c3527..51768e4d 100755 --- a/backend/app/Mail/templates/ResetPassword.php +++ b/backend/app/Mail/templates/ResetPassword.php @@ -32,6 +32,7 @@ namespace MythicalClient\Mail\templates; use MythicalClient\App; +use MythicalClient\Chat\Mails; use MythicalClient\Chat\User; use MythicalClient\Mail\Mail; use MythicalClient\Chat\Database; @@ -46,6 +47,7 @@ public static function sendMail(string $uuid, string $resetToken): void $template = self::getFinalTemplate($uuid); $template = str_replace('${token}', $resetToken, $template); $email = User::getInfo(User::getTokenFromUUID($uuid), UserColumns::EMAIL, false); + Mails::add('Password Reset', $template, $uuid); self::send($email, 'Password Reset', $template); } catch (\Exception $e) { App::getInstance(true)->getLogger()->error('(' . APP_SOURCECODE_DIR . '/Mail/templates/ResetPassword.php) [sendMail] Failed to send email: ' . $e->getMessage()); diff --git a/backend/app/Mail/templates/Verify.php b/backend/app/Mail/templates/Verify.php index 12c9adc2..04e010bf 100755 --- a/backend/app/Mail/templates/Verify.php +++ b/backend/app/Mail/templates/Verify.php @@ -32,6 +32,7 @@ namespace MythicalClient\Mail\templates; use MythicalClient\App; +use MythicalClient\Chat\Mails; use MythicalClient\Chat\User; use MythicalClient\Mail\Mail; use MythicalClient\Chat\Database; @@ -45,6 +46,7 @@ public static function sendMail(string $uuid, string $verifyToken): void $template = self::getFinalTemplate($uuid); $template = str_replace('${token}', $verifyToken, $template); $email = User::getInfo(User::getTokenFromUUID($uuid), UserColumns::EMAIL, false); + Mails::add('Verify your email', $template, $uuid); self::send($email, 'Verify your email', $template); } catch (\Exception $e) { App::getInstance(true)->getLogger()->error('(' . APP_SOURCECODE_DIR . '/Mail/templates/Verify.php) [sendMail] Failed to send email: ' . $e->getMessage()); diff --git a/frontend/src/components/client/Dashboard/Account/Activities.vue b/frontend/src/components/client/Dashboard/Account/Activities.vue index 71283c94..c60dd8db 100755 --- a/frontend/src/components/client/Dashboard/Account/Activities.vue +++ b/frontend/src/components/client/Dashboard/Account/Activities.vue @@ -4,6 +4,11 @@ import { format } from 'date-fns'; import LayoutAccount from './Layout.vue'; import TableTanstack from '@/components/client/ui/Table/TableTanstack.vue'; import Activities from '@/mythicalclient/Activities'; +import { useI18n } from 'vue-i18n'; + +const { t } = useI18n(); + +document.title = t('account.pages.activity.page.title'); interface Activity { id: number; @@ -24,7 +29,7 @@ const fetchActivities = async () => { const response = await Activities.get(); activities.value = response; } catch (err) { - error.value = err instanceof Error ? err.message : 'An unknown error occurred'; + error.value = err instanceof Error ? err.message : t('account.pages.activity.page.table.error'); } finally { loading.value = false; } @@ -41,15 +46,16 @@ onErrorCaptured((err) => { const columnsActivities = [ { accessorKey: 'action', - header: 'Action', + header: t('account.pages.activity.page.table.columns.action'), }, { accessorKey: 'ip_address', - header: 'Ip Address', + header: t('account.pages.activity.page.table.columns.ip'), }, { accessorKey: 'date', - header: 'Created', + header: t('account.pages.activity.page.table.columns.date'), + // eslint-disable-next-line @typescript-eslint/no-explicit-any cell: (info: any) => format(new Date(info.getValue()), 'MMM d, yyyy HH:mm'), }, ]; @@ -69,7 +75,7 @@ const columnsActivities = [ </div> <div v-else class="overflow-x-auto"> - <TableTanstack :data="activities" :columns="columnsActivities" tableName="Activities" /> + <TableTanstack :data="activities" :columns="columnsActivities" :tableName="t('account.pages.activity.page.title')" /> </div> </div> </template> diff --git a/frontend/src/components/client/Dashboard/Account/ActivityTable.vue b/frontend/src/components/client/Dashboard/Account/ActivityTable.vue deleted file mode 100755 index 2bed8523..00000000 --- a/frontend/src/components/client/Dashboard/Account/ActivityTable.vue +++ /dev/null @@ -1,49 +0,0 @@ -<script setup lang="ts"> -import { format } from 'date-fns' - -interface Activity { - id: number - user: string - action: string - ip_address: string - deleted: string - locked: string - date: string -} - -defineProps<{ - activities: Activity[] -}>() -</script> - -<template> - <div class="overflow-x-auto bg-gray-900 rounded-lg shadow"> - <table class="min-w-full divide-y divide-gray-800"> - <thead class="bg-gray-800"> - <tr> - <th scope="col" - class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider"> - Action - </th> - <th scope="col" - class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider"> - IP Address - </th> - <th scope="col" - class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider"> - Date - </th> - </tr> - </thead> - <tbody class="bg-gray-900 divide-y divide-gray-800"> - <tr v-for="activity in activities" :key="activity.id" class="hover:bg-gray-800 transition-colors"> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ activity.action }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ activity.ip_address }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300"> - {{ format(new Date(activity.date), 'MMM d, yyyy HH:mm:ss') }} - </td> - </tr> - </tbody> - </table> - </div> -</template> \ No newline at end of file diff --git a/frontend/src/components/client/Dashboard/Account/ApiKeys.vue b/frontend/src/components/client/Dashboard/Account/ApiKeys.vue deleted file mode 100755 index 9c42f46b..00000000 --- a/frontend/src/components/client/Dashboard/Account/ApiKeys.vue +++ /dev/null @@ -1,71 +0,0 @@ -<script setup lang="ts"> -import LayoutAccount from './Layout.vue'; -import TableTanstack from '@/components/client/ui/Table/TableTanstack.vue'; -import { format } from 'date-fns'; -const columnsApis = [ - { - accessorKey: 'name', - header: 'Name', - }, - { - accessorKey: 'type', - header: 'Type', - }, - { - accessorKey: 'value', - header: 'Value', - }, - { - accessorKey: 'date', - header: 'Date', - cell: (info: { getValue: () => string | number | Date }) => format(new Date(info.getValue()), 'MMM d, yyyy'), - }, -]; - -const Apis = [ - { - name: 'API Key 1', - type: 'Read/Write', - value: '1234567890', - date: '2023-10-01', - }, - { - name: 'API Key 2', - type: 'Read', - value: '0987654321', - date: '2023-10-02', - }, - { - name: 'API Key 3', - type: 'Write', - value: '1234567890', - date: '2023-10-03', - }, -]; -</script> - -<template> - <!-- User Info --> - <LayoutAccount /> - - <div> - <div class="overflow-x-auto"> - <TableTanstack :data="Apis" :columns="columnsApis" tableName="Api Keys" /> - </div> - </div> -</template> - -<style scoped> -/* Hide scrollbar for Chrome, Safari and Opera */ -.overflow-x-auto::-webkit-scrollbar { - display: none; -} - -/* Hide scrollbar for IE, Edge and Firefox */ -.overflow-x-auto { - -ms-overflow-style: none; - /* IE and Edge */ - scrollbar-width: none; - /* Firefox */ -} -</style> diff --git a/frontend/src/components/client/Dashboard/Account/Mails.vue b/frontend/src/components/client/Dashboard/Account/Mails.vue index 52cb8bd8..264b33f3 100755 --- a/frontend/src/components/client/Dashboard/Account/Mails.vue +++ b/frontend/src/components/client/Dashboard/Account/Mails.vue @@ -1,61 +1,91 @@ <script setup lang="ts"> import LayoutAccount from './Layout.vue'; import TableTanstack from '@/components/client/ui/Table/TableTanstack.vue'; -import ViewButton from '@/components/client/ui/Table/ViewButton.vue'; +import Mails from '@/mythicalclient/Mails'; import { format } from 'date-fns'; -import { h } from 'vue'; +import { h, onErrorCaptured, onMounted, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; -const emails = [ - { - subject: 'Welcome to our service', - sender: 'admin@example.com', - created_at: '2023-10-01', - }, - { - subject: 'Your account has been updated', - sender: 'support@example.com', - created_at: '2023-10-02', - }, - { - subject: 'Password reset instructions', - sender: 'no-reply@example.com', - created_at: '2023-10-03', - }, +const { t } = useI18n(); + +document.title = t('account.pages.emails.page.title'); + +interface Email { + id: string; + subject: string; + from: string; + date: string; +} +const emails = ref<Email[]>([]); +const loading = ref(true); +const error = ref<string | null>(null); + +const fetchMails = async () => { + try { + const response = await Mails.get(); + emails.value = response; + console.log(response); + } catch (err) { + error.value = err instanceof Error ? err.message : 'An unknown error occurred'; + } finally { + loading.value = false; + } +}; + +onMounted(fetchMails); + +onErrorCaptured((err) => { + error.value = t('account.pages.emails.alerts.error.generic'); + console.error('Error captured:', err); + return false; +}); + +const columnsEmails = [ { - subject: 'New feature announcement', - sender: 'news@example.com', - created_at: '2023-10-04', + accessorKey: t('account.pages.emails.page.table.columns.id'), + header: 'ID', }, -]; -const columnsEmails = [ { accessorKey: 'subject', - header: 'Subject', + header: t('account.pages.emails.page.table.columns.subject'), }, { - accessorKey: 'sender', - header: 'Sender', + accessorKey: 'from', + header: t('account.pages.emails.page.table.columns.from'), }, { - accessorKey: 'created_at', - header: 'Created', + accessorKey: 'date', + header: t('account.pages.emails.page.table.columns.date'), cell: (info: { getValue: () => string | number | Date }) => format(new Date(info.getValue()), 'MMM d, yyyy'), }, { accessorKey: 'actions', - header: 'Actions', - cell: ({ row }: { row: { original: { id: string } } }) => h(ViewButton, { id: row.original.id }), + header: t('account.pages.emails.page.table.columns.actions'), enableSorting: false, + cell: ({ row }: { row: { original: Email } }) => h('button', { + class: 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600', target: '_blank', rel: 'noopener noreferrer', + onClick: () => window.location.href = `/api/user/session/emails/${row.original.id}/raw` + }, t('account.pages.emails.page.table.results.viewButton')), }, ]; + + </script> <template> - <!-- User Info --> <LayoutAccount /> <div> - <div class="overflow-x-auto"> - <TableTanstack :data="emails" :columns="columnsEmails" tableName="Emails" /> + <div v-if="loading" class="text-center py-4"> + <div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900"></div> + <p class="mt-2">Loading emails...</p> + </div> + + <div v-else-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded" role="alert"> + <p>{{ error }}</p> + </div> + + <div v-else class="overflow-x-auto"> + <TableTanstack :data="emails" :columns="columnsEmails" :tableName="t('account.pages.emails.page.title')" /> </div> </div> </template> diff --git a/frontend/src/components/client/Dashboard/Account/Security.vue b/frontend/src/components/client/Dashboard/Account/Security.vue index 3771ceb7..8219bd8b 100755 --- a/frontend/src/components/client/Dashboard/Account/Security.vue +++ b/frontend/src/components/client/Dashboard/Account/Security.vue @@ -4,10 +4,14 @@ import LayoutAccount from './Layout.vue'; import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; import { useRouter } from 'vue-router'; import Session from '@/mythicalclient/Session'; +import { useI18n } from 'vue-i18n'; const router = useRouter(); +const { t } = useI18n(); const is2FAEnabled = Session.getInfo('2fa_enabled') === 'true' ? ref(true) : ref(false); +document.title = t('account.pages.security.page.title'); + const enable2FA = () => { // Add logic to enable 2FA is2FAEnabled.value = true; @@ -41,36 +45,36 @@ const disable2FA = () => { <LayoutAccount /> <!-- Change Password --> - <CardComponent cardTitle="Change your Password" cardDescription="You want to change the password of your account?"> + <CardComponent :cardTitle="t('account.pages.security.page.cards.password.title')" :cardDescription="t('account.pages.security.page.cards.password.subTitle')"> <router-link to="/auth/forgot-password" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded text-sm font-medium transition-colors" > - Change Password + {{ t('account.pages.security.page.cards.password.change_button.label') }} </router-link> </CardComponent> <br /> <!-- Two-Factor Authentication (2FA) --> <CardComponent - cardTitle="Two-Factor Authentication (2FA)" - cardDescription="You want to be sure your account will not be stolen?" + :cardTitle="t('account.pages.security.page.cards.twofactor.title')" + :cardDescription="t('account.pages.security.page.cards.twofactor.subTitle')" > <div v-if="is2FAEnabled" class="flex items-center justify-between"> - <p class="text-sm text-gray-100">2FA is currently enabled.</p> + <p class="text-sm text-gray-100">{{ t('account.pages.security.page.cards.twofactor.disable_button.description')}}<</p> <button @click="disable2FA" class="ml-4 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors" > - Disable 2FA + {{ t('account.pages.security.page.cards.twofactor.disable_button.label')}} </button> </div> <div v-else class="flex items-center justify-between"> - <p class="text-sm text-gray-100">2FA is currently disabled.</p> + <p class="text-sm text-gray-100">{{ t('account.pages.security.page.cards.twofactor.enable_button.description')}}</p> <button @click="enable2FA" class="ml-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium transition-colors" > - Enable 2FA + {{ t('account.pages.security.page.cards.twofactor.enable_button.label') }} </button> </div> </CardComponent> diff --git a/frontend/src/components/client/Dashboard/Account/Settings.vue b/frontend/src/components/client/Dashboard/Account/Settings.vue index 1ff5eb62..c12255c8 100755 --- a/frontend/src/components/client/Dashboard/Account/Settings.vue +++ b/frontend/src/components/client/Dashboard/Account/Settings.vue @@ -138,7 +138,7 @@ const resetFields = async () => { <div> <label class="block"> <span class="block text-sm font-medium text-gray-400 mb-1.5">{{ - t('account.pages.settings.page.form.background') + t('account.pages.settings.page.form.background.label') }}</span> <TextInput type="url" v-model="form.background" name="background" id="background" /> </label> diff --git a/frontend/src/locale/en.yml b/frontend/src/locale/en.yml index 12fd3476..8e58e152 100755 --- a/frontend/src/locale/en.yml +++ b/frontend/src/locale/en.yml @@ -313,6 +313,73 @@ account: update_button: label: Update reset: Reset + emails: + alerts: + success: + title: Success + update_success: Email preferences updated + footer: Your email preferences have been updated + error: + title: Error + generic: An error occurred. Please try again later + footer: Please contact support for assistance + page: + title: Emails + subTitle: See your emails! + table: + columns: + id: ID + subject: Subject + date: Date + from: From + actions: Actions + empty: No emails found + results: + viewButton: View + security: + alerts: + success: + title: Success + update_success: Security settings updated + footer: Your security settings have been updated + error: + title: Error + generic: An error occurred. Please try again later + footer: Please contact support for assistance + page: + title: Security + subTitle: Manage your security settings + cards: + twofactor: + title: Two-Factor Authentication (2FA) + subTitle: You want to be sure your account will not be stolen? + enable_button: + description: "2FA is currently disabled. Enable it to secure your account." + label: Enable + loading: Enabling... + disable_button: + description: "2FA is currently enabled. Disable it to put your account into a less secure state." + label: Disable + loading: Disabling... + password: + title: Change your password + subTitle: You want to change the password of your account? + change_button: + label: Change Password + loading: Changing... + activity: + page: + title: Activity + subTitle: See your activity! + table: + columns: + action: Action + date: Date + ip: IP + error: An error occurred while fetching your activity + results: + viewButton: View + components: sidebar: dashboard: Dashboard diff --git a/frontend/src/mythicalclient/Mails.ts b/frontend/src/mythicalclient/Mails.ts new file mode 100755 index 00000000..fff84463 --- /dev/null +++ b/frontend/src/mythicalclient/Mails.ts @@ -0,0 +1,16 @@ +class Mails { + /** + * Get the Mails for the current session + * + * @returns The response from the server + */ + public static async get() { + const response = await fetch('/api/user/session/emails', { + method: 'GET', + }); + const data = await response.json(); + return data.emails; + } +} + +export default Mails; \ No newline at end of file diff --git a/frontend/src/views/client/Account.vue b/frontend/src/views/client/Account.vue index fd7332ec..c5bcbc90 100755 --- a/frontend/src/views/client/Account.vue +++ b/frontend/src/views/client/Account.vue @@ -24,7 +24,6 @@ <SecurityTab v-if="activeTab === 'Security'" /> <MailsTab v-if="activeTab === 'Mails'" /> <ActivitiesTab v-if="activeTab === 'Activities'" /> - <ApiKeysTab v-if="activeTab === 'API Keys'" /> <BillingTab v-if="activeTab === 'Billing'" /> </LayoutDashboard> </template> @@ -35,7 +34,6 @@ import SettingsTab from '@/components/client/Dashboard/Account/Settings.vue'; import SecurityTab from '@/components/client/Dashboard/Account/Security.vue'; import MailsTab from '@/components/client/Dashboard/Account/Mails.vue'; import ActivitiesTab from '@/components/client/Dashboard/Account/Activities.vue'; -import ApiKeysTab from '@/components/client/Dashboard/Account/ApiKeys.vue'; import BillingTab from '@/components/client/Dashboard/Account/Billing.vue'; import { ref } from 'vue'; @@ -44,7 +42,6 @@ import { Lock as SecurityIcon, Mail as MailIcon, Bell as ActivityIcon, - Key as ApiKeyIcon, CreditCard as BillingIcon, } from 'lucide-vue-next'; @@ -56,7 +53,6 @@ const tabs = [ { name: 'Security', icon: SecurityIcon }, { name: 'Mails', icon: MailIcon }, { name: 'Activities', icon: ActivityIcon }, - { name: 'API Keys', icon: ApiKeyIcon }, ]; </script>