소스 검색

feat: :sparkles: permissão nas rotas , linguagem e darkmode

Denis 1 년 전
부모
커밋
89fffc829d

+ 1 - 0
.eslintrc.cjs

@@ -63,6 +63,7 @@ module.exports = {
   // add your custom rules here
   rules: {
     "prefer-promise-reject-errors": "off",
+    "vue/require-prop-types": "off",
 
     // allow debugger during development only
     "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",

+ 1 - 1
quasar.config.js

@@ -14,7 +14,7 @@ export default configure((ctx) => {
     // app boot file (/src/boot)
     // --> boot files are part of "main.js"
     // https://v2.quasar.dev/quasar-cli-vite/boot-files
-    boot: ["i18n", "axios", "importGlobalComponents"],
+    boot: ["i18n", "axios", "importGlobalComponents", "setPermissions"],
 
     // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
     css: ["app.scss"],

+ 33 - 1
src/App.vue

@@ -3,7 +3,39 @@
 </template>
 
 <script setup>
+import { Cookies, useQuasar } from "quasar";
+import { watch } from "vue";
+import { useI18n } from "vue-i18n";
+
 defineOptions({
-  name: 'App'
+  name: "App",
 });
+
+const { locale } = useI18n();
+
+const $q = useQuasar();
+const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
+  ? "dark"
+  : "light";
+
+const theme = $q.cookies.get("theme") || systemTheme;
+$q.dark.set(theme === "dark");
+
+watch(
+  () => $q.dark.isActive,
+  (value) => {
+    $q.cookies.set("theme", value ? "dark" : "light", {
+      expires: 365,
+    });
+  },
+);
+
+watch(
+  () => locale.value,
+  (value) => {
+    Cookies.set("locale", value, {
+      expires: 365,
+    });
+  },
+);
 </script>

+ 5 - 9
src/api/permission.js

@@ -1,15 +1,11 @@
 import { api } from "src/boot/axios";
 
 export const getGuestPermissions = async () => {
-  const { data } = await api.get("/user/permissions/guest");
-  return data;
+  const { data } = await api.get("/permissions-by-type/guest");
+  return data.payload;
 };
 
-export const getUserPermissions = async (userType) => {
-  const { data } = await api.get("/user/permissions", {
-    params: {
-      userType,
-    },
-  });
-  return data;
+export const getUserPermissions = async () => {
+  const { data } = await api.get("/permissions-by-type");
+  return data.payload;
 };

+ 8 - 3
src/api/user.js

@@ -1,16 +1,21 @@
 import { api } from "src/boot/axios";
 
 export const getUser = async () => {
+  const { data } = await api.get("/user/me");
+  return data.payload;
+};
+
+export const getUsers = async () => {
   const { data } = await api.get("/user");
-  return data;
+  return data.payload;
 };
 
 export const createUser = async (user) => {
   const { data } = await api.post("/user", user);
-  return data;
+  return data.payload;
 };
 
 export const updateUser = async (user, id) => {
   const { data } = await api.put(`/user/${id}`, user);
-  return data;
+  return data.payload;
 };

+ 39 - 11
src/boot/axios.js

@@ -8,20 +8,28 @@ import axios from "axios";
 // good idea to move this instance creation inside of the
 // "export default () => {}" function below (which runs individually
 // for each client)
+
+let isRefreshing = false;
+let failedQueue = [];
+
+const processQueue = (error, token = null) => {
+  failedQueue.forEach((prom) =>
+    error ? prom.reject(error) : prom.resolve(token),
+  );
+  failedQueue = [];
+};
+
 const api = axios.create({
   baseURL: process.env.API_URL + "/api",
   withCredentials: true,
   withXSRFToken: true,
-  headers: {
-    language: Cookies.get("language")
-      ? Cookies.get("language")
-      : window.navigator.language,
-  },
 });
 
 api.interceptors.request.use(
   (config) => {
     const access_token = Cookies.get("access_token");
+    const language = Cookies.get("locale") || window.navigator.language;
+    config.headers["Accept-Language"] = language;
     if (access_token) {
       config.headers.Authorization = `Bearer ${access_token}`;
     }
@@ -32,19 +40,39 @@ api.interceptors.request.use(
   },
 );
 
-const errorInterceptor = async (error, app) => {
+const errorInterceptor = async (error) => {
   if (error.response.status === 401) {
-    const isRefreshValid = await useAuth().refreshToken();
-    if (!isRefreshValid) {
-      app.config.globalProperties.$router.push({ name: "Login" });
-    } else {
-      return api.request(error.config);
+    if (!isRefreshing) {
+      isRefreshing = true;
+      const isRefreshValid = await useAuth().refreshToken();
+      isRefreshing = false;
+      if (!isRefreshValid) {
+        Cookies.remove("access_token");
+        Cookies.remove("refresh_token");
+        window.location.href = "/login";
+        processQueue(new Error("Token refresh failed"), null);
+        return Promise.reject(new Error("Token refresh failed"));
+      } else {
+        processQueue(null, true);
+      }
     }
+
+    return new Promise((resolve, reject) => {
+      failedQueue.push({ resolve, reject });
+    })
+      .then(() => {
+        return api.request(error.config);
+      })
+      .catch((err) => {
+        return Promise.reject(err);
+      });
   }
+
   Notify.create({
     message: error.response.data.message,
     type: "negative",
   });
+
   return Promise.reject(error);
 };
 

+ 4 - 1
src/boot/i18n.js

@@ -1,10 +1,13 @@
 import { boot } from "quasar/wrappers";
 import { createI18n } from "vue-i18n";
+import { Cookies } from "quasar";
 import messages from "src/i18n";
 
 export default boot(({ app }) => {
   const i18n = createI18n({
-    locale: window.navigator.language,
+    locale: Cookies.get("locale")
+      ? Cookies.get("locale")
+      : window.navigator.language,
     globalInjection: true,
     messages,
   });

+ 6 - 0
src/boot/setPermissions.js

@@ -0,0 +1,6 @@
+import { boot } from "quasar/wrappers";
+import { permissionStore } from "src/stores/permission";
+
+export default boot(async () => {
+  await permissionStore().fetchScopes();
+});

+ 2 - 2
src/components/global/PasswordField.vue → src/components/PasswordField.vue

@@ -4,7 +4,7 @@
     v-model="form.password"
     class="col-6"
     filled
-    label="Senha"
+    :label="$t('users.password')"
     :rules="[inputRules.required]"
     hide-bottom-space
     :type="isPwd ? 'password' : 'text'"
@@ -23,7 +23,7 @@
     v-model="form.password_confirmation"
     class="col-6"
     filled
-    label="Confirmar Senha"
+    :label="$t('general.confirm_password')"
     :rules="[inputRules.required, confirmedPassword]"
     hide-bottom-space
     lazy-rules

+ 34 - 88
src/components/global/DefaultTable.vue

@@ -2,16 +2,12 @@
   <q-table
     v-model:fullscreen="fullscreen"
     flat
-    :class="
-      props.table
-        ? 'softpar-table bg-background table-bottom'
-        : 'softpar-table bg-background '
-    "
+    class="softpar-table"
     :pagination="{ rowsPerPage }"
     :pagination-label="getPaginationLabel"
     row-key="id"
     :rows="rows"
-    :rows-per-page-label="props.rowsPerPageLabel"
+    :rows-per-page-label="$t('general.rows_per_page')"
     :columns="props.columns"
     :visible-columns="visibleColumns"
     :filter="filter"
@@ -24,7 +20,7 @@
         outlined
         dense
         debounce="500"
-        placeholder="Buscar"
+        :placeholder="$t('general.search')"
         style="min-width: 400px"
         clearable
         autofocus
@@ -34,14 +30,14 @@
         </template>
       </q-input>
 
-      <q-checkbox
+      <!-- <q-checkbox
         v-if="props.mostrarToggleInativos"
         v-model="showInativos"
         class="q-ml-sm"
         :label="props.labelInativo"
         dense
         color="secondary"
-      />
+      /> -->
 
       <q-select
         v-if="mostrarSelecaoDeColunas"
@@ -75,7 +71,7 @@
         v-if="props.dropDown"
         class="q-mr-md"
         color="primary"
-        label="opcoes"
+        :label="$t('general.options')"
       />
 
       <q-btn
@@ -84,7 +80,7 @@
         color="primary"
         padding="12px 16px"
         :outline="props.outlineAdd"
-        :label="props.labelAdd"
+        :label="$t('general.add')"
         @click="onAddItem"
       >
       </q-btn>
@@ -95,10 +91,10 @@
         <q-item-section>
           <span class="text-center">
             <div v-if="row.status && value" class="ativo body2 text-positive">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
             <div v-if="!row.status" class="inativo body2 text-accent">
-              {{ $t("inactive") }}
+              {{ $t("general.inactive") }}
             </div>
           </span>
         </q-item-section>
@@ -110,50 +106,25 @@
         <q-item-section>
           <span class="text-center">
             <div v-if="row.ativo && value" class="ativo body2 text-positive">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
             <div v-if="row.ativo && !value" class="ativo body2 text-positive">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
             <div v-if="!row.ativo" class="inativo body2 text-accent">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
           </span>
         </q-item-section>
       </q-td>
     </template>
 
-    <template #body-cell-principal="{ value, row }">
-      <q-td style="width: 1%">
-        <q-item-section>
-          <span class="text-center">
-            <q-icon
-              v-if="row.principal && value"
-              name="mdi-star"
-              size="1.5rem"
-              style="color: #385873"
-              onmouseover="this.style.color='#688FAF';"
-              onmouseout="this.style.color='#385873';"
-              @click.stop="togglePrincipal(row)"
-            />
-            <q-icon
-              v-if="!row.principal"
-              name="mdi-star-outline"
-              size="1.5rem"
-              style="color: #385873"
-              onmouseover="this.style.color='#688FAF';"
-              onmouseout="this.style.color='#385873';"
-              @click.stop="togglePrincipal(row)"
-            />
-          </span>
-        </q-item-section>
-      </q-td>
-    </template>
-
     <template v-if="!props.hideNoDataLabel" #no-data>
       <div class="q-my-md row justify-center full-width">
         <q-spinner v-if="loading" color="primary" size="30px" />
-        <div v-else class="q-pa-md body2">{{ $t("no_records_found") }}</div>
+        <div v-else class="q-pa-md body2">
+          {{ $t("errors.no_records_found") }}
+        </div>
       </div>
     </template>
 
@@ -166,14 +137,8 @@
 <script setup>
 import { ref, onMounted, toRaw, watch } from "vue";
 import { useRouter } from "vue-router";
-import { api } from "boot/axios";
 
-const emit = defineEmits([
-  "onRowClick",
-  "onAddItem",
-  "noRows",
-  "togglePrincipal",
-]);
+const emit = defineEmits(["onRowClick", "onAddItem", "noRows"]);
 
 const props = defineProps({
   // colunas de configuração da tabela
@@ -184,15 +149,10 @@ const props = defineProps({
 
   // rota da api, ex: /clientes
   apiRoute: {
-    type: String,
+    type: Function,
     required: true,
   },
 
-  labelAdd: {
-    type: String,
-    default: "Adicionar",
-  },
-
   // botao de adicionar com aparencia de outline
   outlineAdd: {
     type: Boolean,
@@ -249,10 +209,10 @@ const props = defineProps({
     default: false,
   },
 
-  mostrarToggleInativos: {
-    type: Boolean,
-    default: false,
-  },
+  // mostrarToggleInativos: {
+  //   type: Boolean,
+  //   default: false,
+  // },
 
   mostrarCampoPesquisa: {
     type: Boolean,
@@ -264,24 +224,15 @@ const props = defineProps({
     default: false,
   },
 
-  table: {
-    type: Boolean,
-    default: false,
-  },
-
-  rowsPerPageLabel: {
-    type: String,
-    default: "Linhas por página",
-  },
-
   hideNoDataLabel: {
     type: Boolean,
     default: false,
   },
-  labelInativo: {
-    type: String,
-    default: "Exibir inativos",
-  },
+
+  // labelInativo: {
+  //   type: String,
+  //   default: "Exibir inativos",
+  // },
 });
 
 const router = useRouter();
@@ -358,16 +309,16 @@ const onRequest = async () => {
   loading.value = true;
 
   // pega os dados do servidor
-  const response = await api.get(`${props.apiRoute}`);
+  const response = await props.apiRoute();
   // limpa os dados atuais e adiciona os novos
-  rows.value.splice(0, rows.value.length, ...response.data.result);
+  rows.value.splice(0, rows.value.length, ...response);
 
-  if (props.mostrarToggleInativos && !showInativos.value) {
-    inativos.value = rows.value.filter(
-      (row) => row.status === false || row.ativo === false,
-    );
-    rows.value = rows.value.filter((row) => row.ativo || row.status === true);
-  }
+  // if (props.mostrarToggleInativos && !showInativos.value) {
+  //   inativos.value = rows.value.filter(
+  //     (row) => row.status === false || row.ativo === false,
+  //   );
+  //   rows.value = rows.value.filter((row) => row.ativo || row.status === true);
+  // }
 
   if (rows.value.length == 0) {
     emit("noRows");
@@ -376,11 +327,6 @@ const onRequest = async () => {
   loading.value = false;
 };
 
-// funcao exclusiva para contatos table, para alterar o contato principal
-const togglePrincipal = async (row) => {
-  emit("togglePrincipal", row);
-};
-
 onMounted(() => {
   // faz a primeira requisição
   onRequest({

+ 26 - 68
src/components/global/DefaultTableServerSide.vue

@@ -11,7 +11,7 @@
     "
     :pagination-label="getPaginationLabel"
     :rows="rows"
-    :rows-per-page-label="props.rowsPerPageLabel"
+    :rows-per-page-label="$t('general.rows_per_page')"
     :columns="props.columns"
     :visible-columns="visibleColumns"
     :filter="pagination.filter"
@@ -35,14 +35,14 @@
         </template>
       </q-input>
 
-      <q-checkbox
+      <!-- <q-checkbox
         v-if="props.mostrarToggleInativos"
         v-model="pagination.showInativos"
         class="q-ml-sm"
         :label="props.labelInativo"
         dense
         color="secondary"
-      />
+      /> -->
 
       <q-select
         v-if="mostrarSelecaoDeColunas"
@@ -96,10 +96,10 @@
         <q-item-section>
           <span class="text-center">
             <div v-if="row.status && value" class="ativo body2 text-positive">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
             <div v-if="!row.status" class="inativo body2 text-accent">
-              {{ $t("inactive") }}
+              {{ $t("general.inactive") }}
             </div>
           </span>
         </q-item-section>
@@ -111,57 +111,32 @@
         <q-item-section>
           <span class="text-center">
             <div v-if="row.ativo && value" class="ativo body2 text-positive">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
             <div v-if="row.ativo && !value" class="ativo body2 text-positive">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
             <div v-if="!row.ativo" class="inativo body2 text-accent">
-              {{ $t("active") }}
+              {{ $t("general.active") }}
             </div>
           </span>
         </q-item-section>
       </q-td>
     </template>
 
-    <template #body-cell-principal="{ value, row }">
-      <q-td style="width: 1%">
-        <q-item-section>
-          <span class="text-center">
-            <q-icon
-              v-if="row.principal && value"
-              name="mdi-star"
-              size="1.5rem"
-              style="color: #385873"
-              onmouseover="this.style.color='#688FAF';"
-              onmouseout="this.style.color='#385873';"
-              @click.stop="togglePrincipal(row)"
-            />
-            <q-icon
-              v-if="!row.principal"
-              name="mdi-star-outline"
-              size="1.5rem"
-              style="color: #385873"
-              onmouseover="this.style.color='#688FAF';"
-              onmouseout="this.style.color='#385873';"
-              @click.stop="togglePrincipal(row)"
-            />
-          </span>
-        </q-item-section>
-      </q-td>
-    </template>
-
     <template v-if="!props.hideNoDataLabel" #no-data>
       <div class="q-my-md row justify-center full-width">
         <q-spinner v-if="loading" color="primary" size="30px" />
-        <div v-else class="q-pa-md body2">{{ $t("no_records_found") }}</div>
+        <div v-else class="q-pa-md body2">
+          {{ $t("errors.no_records_found") }}
+        </div>
       </div>
     </template>
 
     <template #bottom="scope">
       <div class="flex full-width justify-end">
         <div class="flex items-center">
-          {{ props.rowsPerPageLabel }}
+          {{ $t("general.rows_per_page") }}
           <q-select
             v-model="pagination.rowsPerPage"
             class="q-mx-sm"
@@ -173,7 +148,7 @@
               <q-item v-bind="selectData.itemProps">
                 <q-item-section>
                   <q-item-label>{{
-                    selectData.opt == 0 ? $t("all") : selectData.opt
+                    selectData.opt == 0 ? $t("general.all") : selectData.opt
                   }}</q-item-label>
                 </q-item-section>
               </q-item>
@@ -181,7 +156,7 @@
           </q-select>
         </div>
         <div class="flex items-center">
-          {{ pagination.from + "-" + pagination.to }} {{ $t("of") }}
+          {{ pagination.from + "-" + pagination.to }} {{ $t("labels.of") }}
           {{ pagination.rowsNumber }}
         </div>
         <div class="flex items-center">
@@ -216,7 +191,6 @@
 <script setup>
 import { ref, onMounted, toRaw, watch } from "vue";
 import { useRouter } from "vue-router";
-import { api } from "boot/axios";
 
 const emit = defineEmits([
   "onRowClick",
@@ -234,7 +208,7 @@ const props = defineProps({
 
   // rota da api, ex: /clientes
   apiRoute: {
-    type: String,
+    type: Function,
     required: true,
   },
 
@@ -299,10 +273,10 @@ const props = defineProps({
     default: false,
   },
 
-  mostrarToggleInativos: {
-    type: Boolean,
-    default: false,
-  },
+  // mostrarToggleInativos: {
+  //   type: Boolean,
+  //   default: false,
+  // },
 
   mostrarCampoPesquisa: {
     type: Boolean,
@@ -319,19 +293,15 @@ const props = defineProps({
     default: false,
   },
 
-  rowsPerPageLabel: {
-    type: String,
-    default: "Linhas por página",
-  },
-
   hideNoDataLabel: {
     type: Boolean,
     default: false,
   },
-  labelInativo: {
-    type: String,
-    default: "Exibir inativos",
-  },
+
+  // labelInativo: {
+  //   type: String,
+  //   default: "Exibir inativos",
+  // },
 });
 
 const router = useRouter();
@@ -445,16 +415,9 @@ const onRequest = async () => {
   loading.value = true;
 
   // pega os dados do servidor
-  const response = await api.get(`${props.apiRoute}`, {
-    params: {
-      filter: pagination.value.filter,
-      page: pagination.value.page,
-      rowsPerPage: pagination.value.rowsPerPage,
-      exibirInativos: pagination.value.showInativos,
-    },
-  });
+  const response = await props.apiRoute();
   // limpa os dados atuais e adiciona os novos
-  rows.value.splice(0, rows.value.length, ...response.data.result.data);
+  rows.value.splice(0, rows.value.length, ...response);
 
   pagination.value.rowsNumber = response.data.result.total;
   pagination.value.from = response.data.result.from;
@@ -467,11 +430,6 @@ const onRequest = async () => {
   loading.value = false;
 };
 
-// funcao exclusiva para contatos table, para alterar o contato principal
-const togglePrincipal = async (row) => {
-  emit("togglePrincipal", row);
-};
-
 onMounted(async () => {
   await onRequest();
   if (props.comecarDesativado) {

+ 12 - 20
src/components/global/LeftMenuLayout.vue

@@ -7,7 +7,6 @@
     :width="250"
     :breakpoint="500"
     bordered
-    class="bg-drawer"
     @mouseover="miniState = false"
     @mouseout="miniState = true"
   >
@@ -30,14 +29,14 @@
               <q-icon :name="menu.icon" />
             </q-item-section>
 
-            <q-item-section> {{ menu.title }} </q-item-section>
+            <q-item-section> {{ $t(menu.title) }} </q-item-section>
           </q-item>
           <q-expansion-item
             v-if="menu.type == 'expansive' && menu.permission"
             expand-separator
             header-class="text-subtitle1"
             :icon="menu.icon"
-            :label="menu.title"
+            :label="$t(menu.title)"
             :content-inset-level="0.4"
             :disable="menu.disable"
           >
@@ -54,8 +53,7 @@
                 <q-item-section avatar>
                   <q-icon :name="children.icon" />
                 </q-item-section>
-
-                <q-item-section> {{ children.title }} </q-item-section>
+                <q-item-section> {{ $t(children.title) }} </q-item-section>
               </q-item>
             </template>
           </q-expansion-item>
@@ -70,7 +68,7 @@
             <q-item-section avatar>
               <q-icon name="mdi-logout-variant" color="negative" />
             </q-item-section>
-            <q-item-section> {{ $t("logout") }} </q-item-section>
+            <q-item-section> {{ $t("navigation.logout") }} </q-item-section>
           </div>
         </q-item>
       </q-list>
@@ -82,18 +80,16 @@
 import { ref, onMounted } from "vue";
 import { useAuth } from "src/composables/useAuth";
 import { permissionStore } from "src/stores/permission";
-import { useI18n } from "vue-i18n/dist/vue-i18n";
 
 const auth = useAuth();
-const { t } = useI18n();
 const leftDrawerOpen = ref(true);
 const miniState = ref(true);
 
 const menus = ref([
   {
     type: "single",
-    title: t("dashboard"),
-    name: "DashboardPage",
+    title: "navigation.dashboard",
+    name: "HomePage",
     icon: "mdi-home-variant-outline",
     disable: false,
     permission: false,
@@ -101,7 +97,7 @@ const menus = ref([
   },
   {
     type: "expansive",
-    title: t("registration"),
+    title: "navigation.registration",
     icon: "mdi-cog-outline",
     disable: false,
     permission: false,
@@ -109,12 +105,12 @@ const menus = ref([
     childrens: [
       {
         type: "single",
-        title: t("users"),
-        name: "UsuariosPage",
+        title: "navigation.users",
+        name: "UsersPage",
         icon: "mdi-account-multiple-outline",
         disable: false,
         permission: false,
-        permissionScope: "usuarios",
+        permissionScope: "config.user",
       },
     ],
   },
@@ -134,12 +130,8 @@ const getMenuAccess = () => {
         });
         return menu.childrens.length > 0 ? menu : null;
       } else {
-        if (menu.componente === "vuePageDashboard") {
-          menu.permission = true;
-        } else {
-          menu.permission = getAccess(menu.permissionScope, "menu");
-        }
-        return menu.permission ? menu : null;
+        menu.permission = getAccess(menu.permissionScope, "menu");
+        return menu;
       }
     })
     .filter((menu) => menu !== null);

+ 16 - 13
src/composables/useAuth.js

@@ -1,6 +1,8 @@
 import { api } from "src/boot/axios";
 import { Cookies } from "quasar";
 import { useRouter } from "vue-router";
+import { permissionStore } from "src/stores/permission";
+import { userStore } from "src/stores/user";
 
 export const useAuth = () => {
   const router = useRouter();
@@ -13,21 +15,21 @@ export const useAuth = () => {
       });
 
       if (response.status === 200) {
-        console.log(response.data.payload);
+        const payload = response.data.payload;
         const accessTokenExpiresIn = new Date(
-          new Date().getTime() +
-            response.data.payload.access_token_expires_in * 1000,
+          new Date().getTime() + payload.access_token_expires_in * 1000,
         );
         const refreshTokenExpiresIn = new Date(
-          new Date().getTime() +
-            response.data.payload.refresh_token_expires_in * 1000,
+          new Date().getTime() + payload.refresh_token_expires_in * 1000,
         );
-        Cookies.set("access_token", response.data.payload.access_token, {
+        Cookies.set("access_token", payload.access_token, {
           expires: accessTokenExpiresIn,
         });
-        Cookies.set("refresh_token", response.data.payload.refresh_token, {
+        Cookies.set("refresh_token", payload.refresh_token, {
           expires: refreshTokenExpiresIn,
         });
+        userStore().user = payload.user;
+        await permissionStore().fetchScopes();
       }
     } catch (error) {
       console.error(error);
@@ -40,6 +42,7 @@ export const useAuth = () => {
       if (response.status === 200) {
         Cookies.remove("access_token");
         Cookies.remove("refresh_token");
+        await permissionStore().fetchScopes();
         router.push({ name: "Login" });
       }
     } catch (error) {
@@ -55,20 +58,20 @@ export const useAuth = () => {
       });
 
       if (response.status === 200) {
+        const payload = response.data.payload;
         const accessTokenExpiresIn = new Date(
-          new Date().getTime() +
-            response.data.payload.access_token_expires_in * 1000,
+          new Date().getTime() + payload.access_token_expires_in * 1000,
         );
         const refreshTokenExpiresIn = new Date(
-          new Date().getTime() +
-            response.data.payload.refresh_token_expires_in * 1000,
+          new Date().getTime() + payload.refresh_token_expires_in * 1000,
         );
-        Cookies.set("access_token", response.data.payload.access_token, {
+        Cookies.set("access_token", payload.access_token, {
           expires: accessTokenExpiresIn,
         });
-        Cookies.set("refresh_token", response.data.payload.refresh_token, {
+        Cookies.set("refresh_token", payload.refresh_token, {
           expires: refreshTokenExpiresIn,
         });
+        userStore().user = payload.user;
       }
 
       return true;

+ 16 - 19
src/css/table.scss

@@ -1,19 +1,20 @@
 .softpar-table {
-  .body--dark & {
-  --table-bg-color: #181818;
-  --table-border-color: #3b3b3b;
-  --table-header-color: #f9f9f9;
-}
+  padding-left: 16px !important;
+  padding-right: 16px !important;
 
-.body--light & {
-  --table-bg-color: #f9f9f9;
-  --table-border-color: #e0e0e0;
-  --table-header-color: #717171;
-}
+  .body--dark & {
+    --table-bg-color: #181818;
+    --table-border-color: #3b3b3b;
+    --table-header-color: #f9f9f9;
+  }
 
+  .body--light & {
+    --table-bg-color: #f9f9f9;
+    --table-border-color: #e0e0e0;
+    --table-header-color: #717171;
+  }
 
   :deep(.q-table) {
-
     thead tr:first-child th {
       background-color: var(-q--primary) !important;
     }
@@ -79,7 +80,6 @@
     background: #cfdab7;
     border: none;
   }
-
 }
 
 .inativo {
@@ -113,11 +113,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #F9E3E3;
+    border: 1px solid #f9e3e3;
   }
 
   .body--light & {
-    background: #F9E3E3;
+    background: #f9e3e3;
     border: none;
   }
 }
@@ -133,11 +133,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #E1E9F0;
+    border: 1px solid #e1e9f0;
   }
 
   .body--light & {
-    background: #E1E9F0;
+    background: #e1e9f0;
     border: none;
   }
 }
@@ -183,6 +183,3 @@
     }
   }
 }
-
-
-

+ 44 - 19
src/i18n/locales/en.json

@@ -1,21 +1,46 @@
 {
-  "404": 404,
-  "active": "Active",
-  "all": "All",
-  "dashboard": "Dashboard",
-  "exit": "Exit",
-  "failed": "Action failed",
-  "login": "Login",
-  "logout": "Logout",
-  "of": "of",
-  "password": "Password",
-  "pageNotFound": "Page not found",
-  "registration": "Registration",
-  "success": "Action was successful",
-  "to": "to",
-  "users": "Users",
-  "version": "Version",
-  "inactive": "Inactive",
-  "no_records_found": "No records found",
-  "welcome": "Welcome"
+  "general": {
+    "add": "Add",
+    "options": "Options",
+    "welcome": "Welcome",
+    "version": "Version",
+    "all": "All",
+    "active": "Active",
+    "inactive": "Inactive",
+    "confirm_password": "Confirm Password",
+    "search": "Search",
+    "title": "Title",
+    "rows_per_page": "Rows per page"
+  },
+  "errors": {
+    "404": "Page not found",
+    "no_records_found": "No records found",
+    "failed": "Action failed",
+    "success": "Action was successful"
+  },
+  "navigation": {
+    "dashboard": "Dashboard",
+    "login": "Login",
+    "logout": "Logout",
+    "exit": "Exit",
+    "registration": "Registration",
+    "users": "Users"
+  },
+  "users": {
+    "password": "Password",
+    "getUser": "Get User",
+    "createUser": "Create User",
+    "updateUser": "Update User"
+  },
+  "labels": {
+    "of": "of",
+    "to": "to"
+  },
+  "permissions": {
+    "add": "You don't have permission to add this",
+    "view": "You don't have permission to view this",
+    "edit": "You don't have permission to edit this",
+    "delete": "You don't have permission to delete this",
+    "create": "You don't have permission to create this"
+  }
 }

+ 43 - 19
src/i18n/locales/es.json

@@ -1,21 +1,45 @@
 {
-  "404": 404,
-  "active": "Activo",
-  "all": "Todos",
-  "dashboard": "Tablero",
-  "exit": "Salir",
-  "failed": "La acción falló",
-  "login": "Iniciar sesión",
-  "logout": "Cerrar sesión",
-  "of": "de",
-  "password": "Contraseña",
-  "pageNotFound": "Página no encontrada",
-  "registration": "Registro",
-  "success": "La acción fue exitosa",
-  "to": "a",
-  "users": "Usuarios",
-  "version": "Versión",
-  "inactive": "Inactivo",
-  "no_records_found": "No se encontraron registros",
-  "welcome": "Bienvenido"
+  "general": {
+    "add": "Añadir",
+    "options": "Opciones",
+    "welcome": "Bienvenido",
+    "version": "Versión",
+    "all": "Todos",
+    "active": "Activo",
+    "inactive": "Inactivo",
+    "confirm_password": "Confirmar contraseña",
+    "search": "Buscar",
+    "rows_per_page": "Filas por página"
+  },
+  "errors": {
+    "404": "Página no encontrada",
+    "no_records_found": "No se encontraron registros",
+    "failed": "La acción falló",
+    "success": "La acción fue exitosa"
+  },
+  "navigation": {
+    "dashboard": "Tablero",
+    "login": "Iniciar sesión",
+    "logout": "Cerrar sesión",
+    "exit": "Salir",
+    "registration": "Registro",
+    "users": "Usuarios"
+  },
+  "users": {
+    "password": "Contraseña",
+    "getUser": "Obtener usuario",
+    "createUser": "Crear usuario",
+    "updateUser": "Actualizar usuario"
+  },
+  "labels": {
+    "of": "de",
+    "to": "a"
+  },
+  "permissions": {
+    "add": "No tienes permiso para agregar esto",
+    "view": "No tienes permiso para ver esto",
+    "edit": "No tienes permiso para editar esto",
+    "delete": "No tienes permiso para eliminar esto",
+    "create": "No tienes permiso para crear esto"
+  }
 }

+ 43 - 19
src/i18n/locales/pt.json

@@ -1,21 +1,45 @@
 {
-  "404": 404,
-  "active": "Ativo",
-  "all": "Todos",
-  "dashboard": "Dashboard",
-  "exit": "Sair",
-  "failed": "A ação falhou",
-  "login": "Login",
-  "logout": "Logout",
-  "of": "de",
-  "password": "Senha",
-  "pageNotFound": "Página não encontrada",
-  "registration": "Registro",
-  "success": "A ação foi bem sucedida",
-  "to": "de",
-  "users": "Usuários",
-  "version": "Versão",
-  "inactive": "Inativo",
-  "no_records_found": "Nenhum registro encontrado",
-  "welcome": "Bem-vindo"
+  "general": {
+    "add": "Adicionar",
+    "options": "Opções",
+    "welcome": "Bem-vindo",
+    "version": "Versão",
+    "all": "Todos",
+    "active": "Ativo",
+    "inactive": "Inativo",
+    "confirm_password": "Confirmar senha",
+    "search": "Buscar",
+    "rows_per_page": "Linhas por página"
+  },
+  "errors": {
+    "404": "Página não encontrada",
+    "no_records_found": "Nenhum registro encontrado",
+    "failed": "A ação falhou",
+    "success": "A ação foi bem sucedida"
+  },
+  "navigation": {
+    "dashboard": "Dashboard",
+    "login": "Login",
+    "logout": "Logout",
+    "exit": "Sair",
+    "registration": "Registro",
+    "users": "Usuários"
+  },
+  "users": {
+    "password": "Senha",
+    "getUser": "Obter usuário",
+    "createUser": "Criar usuário",
+    "updateUser": "Atualizar usuário"
+  },
+  "labels": {
+    "of": "de",
+    "to": "de"
+  },
+  "permissions": {
+    "add": "Você não tem permissão para adicionar isso",
+    "view": "Você não tem permissão para visualizar isso",
+    "edit": "Você não tem permissão para editar isso",
+    "delete": "Você não tem permissão para excluir isso",
+    "create": "Você não tem permissão para criar isso"
+  }
 }

+ 56 - 9
src/layouts/MainLayout.vue

@@ -23,13 +23,13 @@
       <q-toolbar>
         <q-toolbar-title>
           <div class="flex justify-between">
-            <div class="flex width-botoes cursor-pointer" @click="() => {}">
+            <div class="flex cursor-pointer" @click="() => {}">
               <span class="text-subtitle1 text-black q-my-auto flex q-pl-sm">{{
                 version
               }}</span>
             </div>
             <div class="flex">
-              <img :src="logo" alt="logo" class="q-my-auto q-ml-xl" />
+              <img :src="darkLogo" alt="logo" class="q-my-auto q-ml-xl" />
               <div class="flex q-ml-sm">
                 <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
                 <span class="q-my-auto"> ® </span>
@@ -38,7 +38,50 @@
                 </span>
               </div>
             </div>
-            <div class="flex justify-end q-my-auto width-botoes"></div>
+            <div class="flex justify-end q-my-auto">
+              <div class="flex flex-center q-mr-md">
+                <q-icon
+                  size="sm"
+                  name="mdi-translate"
+                  class="cursor-pointer"
+                  :class="{
+                    'text-black': !$q.dark.isActive,
+                    'text-white': $q.dark.isActive,
+                  }"
+                />
+                <q-menu anchor="bottom left" self="top left">
+                  <q-item
+                    v-for="(loc, index) in availableLocales"
+                    :key="index"
+                    v-ripple
+                    clickable
+                    :active="selectedLocale === loc"
+                    active-class="text-primary"
+                    @click="changeLocale(loc)"
+                  >
+                    <q-item-section>
+                      {{ loc }}
+                    </q-item-section>
+                  </q-item>
+                </q-menu>
+              </div>
+              <q-btn
+                flat
+                dense
+                round
+                :class="{
+                  'text-black': !$q.dark.isActive,
+                  'text-white': $q.dark.isActive,
+                }"
+                :icon="
+                  $q.dark.isActive
+                    ? 'mdi-white-balance-sunny'
+                    : 'mdi-moon-waning-crescent'
+                "
+                aria-label="Toggle dark mode"
+                @click="$q.dark.toggle()"
+              />
+            </div>
           </div>
         </q-toolbar-title>
       </q-toolbar>
@@ -47,26 +90,30 @@
 </template>
 
 <script setup>
-import { ref, computed } from "vue";
+import { ref } from "vue";
 import { version } from "../../package.json";
 import { format } from "date-fns";
 import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
 import darkLogo from "/src/assets/softpar_logo.png";
-import lightLogo from "/src/assets/logo_softpar_azul.png";
+// import lightLogo from "/src/assets/logo_softpar_azul.png";
 
 const $q = useQuasar();
+const { availableLocales, locale } = useI18n();
+const selectedLocale = ref(locale.value);
 
 defineOptions({
   name: "MainLayout",
 });
 
+const changeLocale = (loc) => {
+  locale.value = loc;
+  selectedLocale.value = loc;
+};
+
 const year = ref(format(new Date(), "yyyy"));
 const leftDrawerOpen = ref(false);
 
-const logo = computed(() => {
-  return $q.dark.isActive ? darkLogo : lightLogo;
-});
-
 function toggleLeftDrawer() {
   leftDrawerOpen.value = !leftDrawerOpen.value;
 }

+ 3 - 2
src/pages/ErrorNotFound.vue

@@ -3,10 +3,11 @@
     class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center"
   >
     <div>
-      <div style="font-size: 30vh">{{ $t("404") }}</div>
+      <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
+      <div style="font-size: 30vh">{{ "404" }}</div>
 
       <div class="text-h2" style="opacity: 0.4">
-        {{ $t("pageNotFound") }}
+        {{ $t("errors.404") }}
       </div>
 
       <q-btn

+ 5 - 0
src/pages/HomePage.vue

@@ -0,0 +1,5 @@
+<template>
+  <q-page> </q-page>
+</template>
+
+<script setup></script>

+ 0 - 34
src/pages/IndexPage.vue

@@ -1,34 +0,0 @@
-<template>
-  <q-page class="column flex-center" style="gap: 10px">
-    <q-btn color="primary" label="GetUsers" @click="getUser()" />
-    <q-btn
-      color="primary"
-      label="CreateUser"
-      @click="
-        createUser({
-          name: 'John Doe',
-          email: 'john@email.com',
-          password: 'password',
-        })
-      "
-    />
-    <q-btn
-      color="primary"
-      label="UpdateUser"
-      @click="
-        updateUser(
-          {
-            name: 'John Doe',
-            email: 'john@email.com',
-            password: 'password',
-          },
-          1,
-        )
-      "
-    />
-  </q-page>
-</template>
-
-<script setup>
-import { getUser, createUser, updateUser } from "src/api/user";
-</script>

+ 3 - 3
src/pages/LoginPage.vue

@@ -6,7 +6,7 @@
       </div>
 
       <q-card-section class="text-center">
-        <div class="text-h4">{{ $t("welcome") }}</div>
+        <div class="text-h4">{{ $t("general.welcome") }}</div>
       </q-card-section>
 
       <q-form
@@ -31,7 +31,7 @@
 
           <q-input
             v-model="password"
-            :label="$t('password')"
+            :label="$t('users.password')"
             filled
             :type="isPwd ? 'password' : 'text'"
             class="q-mt-xs"
@@ -53,7 +53,7 @@
         <q-card-actions align="right">
           <q-btn
             color="primary"
-            :label="$t('login')"
+            :label="$t('navigation.login')"
             size="md"
             padding="md"
             type="submit"

+ 87 - 0
src/pages/UsersPage.vue

@@ -0,0 +1,87 @@
+<template>
+  <q-page padding>
+    <PageToolbar />
+
+    <DefaultTable
+      :key="tableKey"
+      :columns="columns"
+      :mostrar-selecao-de-colunas="false"
+      :mostrar-botao-fullscreen="false"
+      :mostrar-toggle-inativos="false"
+      style="padding-right: 16px"
+      :api-route="getUsers"
+      open-item
+      add-item
+      @on-row-click="onRowClick"
+      @on-add-item="onAddItem"
+    />
+  </q-page>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useQuasar } from "quasar";
+import { useRouter } from "vue-router";
+import { useI18n } from "vue-i18n";
+import { permissionStore } from "src/stores/permission";
+import { getUsers } from "src/api/user";
+
+// const UsuariosAddEditDialog = defineAsyncComponent(
+//   () => import("src/components/cruds/usuarios/UsuariosAddEditDialog.vue"),
+// );
+
+const permission_store = permissionStore();
+const router = useRouter();
+const $q = useQuasar();
+const tableKey = ref(0);
+const { t } = useI18n();
+
+const columns = [
+  {
+    name: "nome",
+    label: "name",
+    field: "name",
+    align: "left",
+    style: "width: 50%",
+    required: true,
+  },
+  {
+    name: "email",
+    label: "email",
+    field: "email",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+];
+
+const onRowClick = ({ row }) => {
+  if (permission_store.getAccess("config.user", "view") === false) {
+    $q.loading.hide();
+    $q.notify({
+      type: "negative",
+      message: t("permissions.view"),
+    });
+    return;
+  }
+  router.push(`/usuarios/${row.id}`);
+};
+
+// const onAddItem = () => {
+//   if (permission_store.getAccess("config.user", "add") === false) {
+//     $q.loading.hide();
+//     $q.notify({
+//       type: "negative",
+//       message: t("permissions.add"),
+//     });
+//     return;
+//   }
+//   $q.dialog({
+//     component: UsuariosAddEditDialog,
+
+//     componentProps: {},
+//   }).onOk(() => {
+//     tableKey.value = tableKey.value + 1;
+//   });
+// };
+</script>

+ 16 - 1
src/router/index.js

@@ -6,7 +6,11 @@ import {
   createWebHashHistory,
 } from "vue-router";
 import routes from "./routes";
-import { Cookies } from "quasar";
+import { Cookies, Notify } from "quasar";]
+import { useI18n } from "vue-i18n";
+import { permissionStore } from "src/stores/permission";
+
+const {t} = useI18n();
 /*
  * If not building with SSR mode, you can
  * directly export the Router instantiation;
@@ -34,10 +38,21 @@ export default route(function (/* { store, ssrContext } */) {
   });
 
   Router.beforeEach(async (to, from, next) => {
+    const { getAccess } = permissionStore();
     const access_token = Cookies.get("access_token");
     if (to.meta.requireAuth && !access_token) {
       return next({ name: "Login" });
     }
+    if (to.meta.requiredPermission) {
+      const permission = getAccess(to.meta.requiredPermission, "view");
+      if (!permission) {
+        Notify.create({
+          message: t("permissions.view"),
+          type: "negative",
+        });
+        return next(from);
+      }
+    }
     return next();
   });
 

+ 14 - 3
src/router/routes.js

@@ -1,3 +1,11 @@
+let sub_routes = [];
+
+const modules = import.meta.glob("./routes/*.route.js", { eager: true });
+// eslint-disable-next-line no-unused-vars
+Object.entries(modules).forEach(([path, definition]) => {
+  sub_routes = sub_routes.concat(definition.default);
+});
+
 const routes = [
   {
     path: "/",
@@ -6,10 +14,13 @@ const routes = [
     children: [
       {
         path: "",
-        name: "Home",
-        component: () => import("pages/IndexPage.vue"),
-        meta: { requireAuth: true },
+        name: "HomePage",
+        component: () => import("pages/HomePage.vue"),
+        meta: {
+          requireAuth: true,
+        },
       },
+      ...sub_routes,
     ],
   },
   {

+ 13 - 0
src/router/routes/users.route.js

@@ -0,0 +1,13 @@
+const routes = [
+  {
+    path: "/users",
+    name: "UsersPage",
+    component: () => import("pages/UsersPage.vue"),
+    meta: {
+      requireAuth: true,
+      requiredPermission: "config.user",
+    },
+  },
+];
+
+export default routes;

+ 19 - 16
src/stores/permission.js

@@ -1,7 +1,8 @@
 import { defineStore } from "pinia";
 import { ref, computed } from "vue";
-import { userStore } from "./user";
+import { userStore } from "src/stores/user";
 import { getUserPermissions, getGuestPermissions } from "src/api/permission";
+import { Cookies } from "quasar";
 
 export const permissionStore = defineStore("permission", () => {
   const bitwisePermissionTable = Object.freeze({
@@ -27,8 +28,9 @@ export const permissionStore = defineStore("permission", () => {
     limit: 0,
     menu: 0,
   });
+
   const originalBitwisePermissions = ref(null);
-  const scopes = ref(null);
+  const permissions = ref(null);
 
   const totalBitwisePermissions = computed(() =>
     Object.values(bitwisePermissionTable).reduce((a, b) => a + b),
@@ -70,22 +72,22 @@ export const permissionStore = defineStore("permission", () => {
     }
   };
 
-  const getAccess = (componentName, permissionType) => {
+  const getAccess = (scopeName, permissionType) => {
     const { isAdmin } = userStore();
 
     if (isAdmin) {
       return true;
     }
 
-    if (scopes.value) {
+    if (permissions.value) {
       let checkPermission = 0;
-      const component = scopes.value.find(
-        (comp) => comp.componentName === componentName,
+      const scope = permissions.value.find(
+        (permission) => permission.scope === scopeName,
       );
 
-      if (component) {
+      if (scope) {
         checkPermission = bitwisePermissionTable[permissionType] || 0;
-        return component.bits & checkPermission ? true : false;
+        return scope.bits & checkPermission ? true : false;
       }
     }
     return false;
@@ -93,21 +95,22 @@ export const permissionStore = defineStore("permission", () => {
 
   const fetchScopes = async () => {
     try {
-      const userId = userStore().user.id;
-      if (userId) {
-        const response = await getUserPermissions(userId);
-        scopes.value = response.payload;
+      const accessToken = Cookies.get("access_token");
+      if (accessToken) {
+        userStore().fetchUser();
+        const response = await getUserPermissions();
+        permissions.value = response;
       } else {
         const response = await getGuestPermissions();
-        scopes.value = response.payload;
+        permissions.value = response;
       }
     } catch (error) {
-      console.error("Error fetching scopes:", error);
+      console.error("Error fetching permissions:", error);
     }
   };
 
   const resetScopes = () => {
-    scopes.value = null;
+    permissions.value = null;
   };
 
   return {
@@ -117,7 +120,7 @@ export const permissionStore = defineStore("permission", () => {
     checkTotalPermissions,
     checkPermission,
     updateBitMasks,
-    scopes,
+    permissions,
     getAccess,
     fetchScopes,
     resetScopes,

+ 1 - 1
src/stores/user.js

@@ -18,7 +18,7 @@ export const userStore = defineStore("user", () => {
 
   const fetchUser = async () => {
     const response = await getUser();
-    setUser(response.payload);
+    setUser(response);
   };
 
   return {