diff --git a/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx b/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx index b4290116..49f7c145 100644 --- a/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx +++ b/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx @@ -1,5 +1,5 @@ import { useContext, useMemo, useState } from "react"; -import { Link, redirect } from "react-router-dom"; +import { Link, redirect, useNavigate } from "react-router-dom"; import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; import { AuthContext } from "@/contexts/Auth"; import "./WelcomeUser.less" @@ -8,6 +8,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu" export function WelcomeUser() { const auth = useContext(AuthContext); + const navigate = useNavigate(); const [profileClicked, setProfileClicked] = useState(false) @@ -24,10 +25,13 @@ export function WelcomeUser() { profileClicked ?
-
{location.href="/profile/"+auth.profile?.user.name}}> +
{navigate("/profile/"+auth.profile?.user.name); setProfileClicked(false)}}> Perfil
-
{auth.signout()}}> + { auth.user?.accessLevel === "ROLE_ADMIN" &&
{navigate("/settings"); setProfileClicked(false)}}> + Configurações +
} +
{auth.signout(); setProfileClicked(false)}}> Sair
diff --git a/src/contexts/Auth/AuthProvider.tsx b/src/contexts/Auth/AuthProvider.tsx index f2442bf4..21983da5 100644 --- a/src/contexts/Auth/AuthProvider.tsx +++ b/src/contexts/Auth/AuthProvider.tsx @@ -71,7 +71,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { if (profile) updateOrganization(); else - setOrganization(null); + updateOrganization(); + //setOrganization(null); setFinishedLogin(true); return profile; diff --git a/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx b/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx index 800aa9c6..89382d84 100644 --- a/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx +++ b/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx @@ -38,7 +38,7 @@ export function GroupContentMaterials() { groupContext.setEditMaterial(data); }, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, }, { @@ -47,7 +47,7 @@ export function GroupContentMaterials() { className: "delete", onSelect: handleDeleteMaterial, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, } ]; @@ -59,10 +59,8 @@ export function GroupContentMaterials() {
{ - groupContext.loggedData.profile.id == groupContext.group.admin.id || groupContext.loggedData.profile?.id == groupContext.group.organization?.admin.id ? + groupContext.group.canEdit && - : - <> }
diff --git a/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx b/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx index 3004b185..9cf32a6e 100644 --- a/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx +++ b/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx @@ -33,7 +33,7 @@ export function GroupContents() { groupContext.setEditContent(data); }, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, }, { @@ -42,7 +42,7 @@ export function GroupContents() { className: "delete", onSelect: handleDeleteContent, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, } ] @@ -53,12 +53,10 @@ export function GroupContents() {
{ - authContext.profile?.id == groupContext.group.admin.id || authContext.profile?.id == groupContext.group.organization?.admin.id ? + groupContext.group.canEdit && - : - <> }
diff --git a/src/pages/ManageProfile/ManageProfile.tsx b/src/pages/ManageProfile/ManageProfile.tsx index 2d5c6ab8..b059d938 100644 --- a/src/pages/ManageProfile/ManageProfile.tsx +++ b/src/pages/ManageProfile/ManageProfile.tsx @@ -110,6 +110,24 @@ export function ManageProfilePage() { } } + const { value: password, isConfirmed } = await SwalUtils.fireModal({ + title: "Edição de perfil", + input: "password", + inputLabel: "Inserir senha para salvar as alterações", + inputPlaceholder: "Insira sua senha", + confirmButtonText: "Confirmar Alterações", + showCancelButton: true, + cancelButtonText: "Cancelar", + allowOutsideClick: true, + showCloseButton: true, + inputAttributes: { + autocapitalize: "off", + autocorrect: "off" + } + }); + if (!isConfirmed) + return; + UniversimeApi.Profile.edit({ profileId: profile.id, name: firstname, @@ -117,19 +135,14 @@ export function ManageProfilePage() { bio, gender: gender || undefined, imageUrl: newImageUrl, + rawPassword: password, }).then(async res => { if (!res.success) throw new Error(res.message); const p = await authContext.updateLoggedUser(); navigate(`/profile/${p!.user.name}`); - }).catch((reason: Error) => { - SwalUtils.fireModal({ - title: "Erro ao salvar alterações de perfil", - text: reason.message, - icon: 'error', - }); - }) + }) } function submitLinkChanges(e: MouseEvent) { diff --git a/src/pages/Recovery/Recovery.tsx b/src/pages/Recovery/Recovery.tsx index 93274342..a169f4bc 100644 --- a/src/pages/Recovery/Recovery.tsx +++ b/src/pages/Recovery/Recovery.tsx @@ -3,12 +3,14 @@ import "./Recovery.css" import { transform } from "@babel/core" import { Translate } from "phosphor-react" import "../singin/signinForm.css" -import {useState} from "react" +import {useState, useContext} from "react" import UniversimeApi from "@/services/UniversimeApi" +import { AuthContext } from "@/contexts/Auth/AuthContext"; import * as SwalUtils from "@/utils/sweetalertUtils" import ReCAPTCHA from "react-google-recaptcha-enterprise"; export default function Recovery(){ + const auth = useContext(AuthContext); const [username, setUsername] = useState("") const [msg, setMsg] = useState(null) @@ -31,7 +33,9 @@ export default function Recovery(){ }) } - const ENABLE_RECAPTCHA = import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"; + const organizationEnv = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any); + const ENABLE_RECAPTCHA = organizationEnv.recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = organizationEnv.recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return(
@@ -57,7 +61,7 @@ export default function Recovery(){ !ENABLE_RECAPTCHA ? null :

- setRecaptchaRef(r) } sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} /> + setRecaptchaRef(r) } sitekey={RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} />
} diff --git a/src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts b/src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts new file mode 100644 index 00000000..1c2ecb6e --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts @@ -0,0 +1,15 @@ +import { type LoaderFunctionArgs } from "react-router-dom"; +import UniversimeApi from "@/services/UniversimeApi"; + +export type EnvironmentsLoaderResponse = { + envDic: {} | undefined; +}; + +export async function EnvironmentsFetch(): Promise { + const environments = await UniversimeApi.Group.listEnvironments({}); + return { envDic: (environments as any).body.environments??{}, }; +} + +export async function EnvironmentsLoader(args: LoaderFunctionArgs) { + return EnvironmentsFetch(); +} diff --git a/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less new file mode 100644 index 00000000..8e76d9da --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less @@ -0,0 +1,183 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); +@import url(/src/layouts/transitions.less); +@import url(/src/layouts/border-radius.less); + +#environments-settings { + + .environments-list { + margin-top: 1rem; + + .environments-item { + + margin: .5rem; + align-items: center; + + h3 { + margin-top: 1.5rem; + margin-bottom: 1rem; + font-size: 1.2rem; + margin-left: 1rem; + align-items: center; + } + + .environments-label { + display: flex; + font-size: 1rem; + margin-bottom: .5rem; + margin-left: 0; + text-align: left; + align-items: center; + } + + &:not(:last-of-type) { + @margin-size: .75rem; + + padding-bottom: @margin-size; + margin-bottom: @margin-size; + border-bottom: solid 1px @card-item-color; + } + + .enabled-delete-wrapper { + + margin-bottom: .5rem; + display: flex; + justify-content: space-between; + margin-left: 2rem; + align-items: center; + + &:not(:last-of-type) { + @margin-size: .5rem; + + padding-bottom: @margin-size; + margin-bottom: @margin-size; + border-bottom: solid 1px @card-item-color; + } + + .enabled-wrapper { + display: flex; + align-items: center; + + .filter-enabled-root { + + + + @radix-switch-width: 2.5rem; + @radix-switch-height: 1.5rem; + @radix-thumb-diameter: @radix-switch-height; + + width: @radix-switch-width; + height: @radix-switch-height; + margin-right: .5rem; + margin-left: .5rem; + + border-radius: 9999px; + border: none; + outline: 2px solid @primary-color; + + box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75); + + cursor: pointer; + + &:focus { + outline: 2px solid @secondary-color; + } + + &:not([data-state='checked']) { + background-color: @card-background-color; + } + + &[data-state='checked'] { + background-color: @card-item-color; + + .filter-enabled-thumb { + transform: translateX(calc(@radix-switch-width - @radix-thumb-diameter)); + background-color: @primary-color; + } + } + + .filter-enabled-thumb { + display: block; + width: @radix-thumb-diameter; + height: @radix-thumb-diameter; + background-color: #FFF; + border-radius: 50%; + outline: 2px solid @primary-color; + + transition: transform 100ms; + will-change: transform; + } + + } + + } + + + + .environments-text-wrapper { + @email-font-size: 1rem; + @email-padding: .5rem; + @trigger-width: 9.25em; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + align-items: center; + + .environments-text-input { + + align-items: center; + font-size: @email-font-size; + padding: @email-padding; + + background-color: @card-item-color; + border: none; + border-radius: .625rem; + + width: calc(100% + @trigger-width + 10em); + + } + } + } + + } + } + + .buttons-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + + margin-top: 1rem; + width: 100%; + + > button { + padding: .5em 1em; + border: none; + + font-size: 1rem; + text-transform: uppercase; + font-weight: @font-weight-bold; + + cursor: pointer; + + & + button { + margin-left: 1rem; + } + + &.submit { + color: @font-color-v1; + background-color: @primary-color; + border-radius: .625rem; + + &[disabled] { + background-color: @card-item-color; + color: @font-color-v2; + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx new file mode 100644 index 00000000..78555e6f --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx @@ -0,0 +1,165 @@ +import { useReducer, useState, useEffect } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import * as Switch from "@radix-ui/react-switch" + +import { SettingsTitle, SettingsDescription } from "@/pages/Settings"; +import UniversimeApi from "@/services/UniversimeApi"; + +import { type EnvironmentsLoaderResponse, EnvironmentsFetch } from "./EnvironmentsLoader"; +import "./EnvironmentsPage.less"; + +export function EnvironmentsPage() { + const data = useLoaderData() as EnvironmentsLoaderResponse; + + const [fetchEnvironmentsItems, setFetchEnvironmentsItems] = useState({}); + const [environmentsItems, setEnvironmentsItems] = useState>([]); + + const [editedItems, setEditedItems] = useReducer((state:any, action:any) => { + switch (action.type) { + case 'RESET': + return {}; + case 'EDIT': + return { ...state, [action.id]: action.value }; + default: + return state; + } + }, {}); + + const canSave = Object.keys(editedItems).length > 0; + + useEffect(() => { + + setFetchEnvironmentsItems(data.envDic); + + setEnvironmentsItems([ + { + title: "Conta", + items: [ + { + name: "Habilitar Registrar-se", + key: "signup_enabled", + type: "boolean", + defaultValue: true, + }, + { + name: "Confirmar Conta ao Registrar-se", + key: "signup_confirm_account_enabled", + type: "boolean", + defaultValue: false, + }, + ] + }, + { + title: "Login via Google", + items: [ + { + name: "Habilitar", + key: "login_google_enabled", + type: "boolean", + defaultValue: false, + }, + { + name: "Client ID", + key: "google_client_id", + type: "string", + defaultValue: "", + }, + ] + }, + { + title: "reCAPTCHA Enterprise", + items: [ + { + name: "Habilitar", + key: "recaptcha_enabled", + type: "boolean", + defaultValue: false, + }, + { + name: "Api Key", + key: "recaptcha_api_key", + type: "string", + defaultValue: "", + }, + { + name: "Api Project Id", + key: "recaptcha_api_project_id", + type: "string", + defaultValue: "", + }, + { + name: "Site Key", + key: "recaptcha_site_key", + type: "string", + defaultValue: "", + }, + ] + }, + ]); + + }, [data]); + + return
+ Variáveis Ambiente + Aqui você pode configurar as variáveis ambiente para algumas funcinalidades da plataforma. + +
+ {environmentsItems.map((section : any) => ( +
+

{section.title}

+ {section.items.map((item : any) => ( +
+
{item.name}
+ { item.type === "boolean" ? ( +
+
+ {getValue(item) ? "Ativado" : "Desativado"} + + + +
+
+ ) : null} + { item.type === "string" ? ( +
+ setTextValue(item, e)} /> +
+ ) : null} +
+ ))} +
+ ))} +
+
+
+ +
+
+ + + function getValue(item: any) { + return editedItems[item.key] !== undefined ? editedItems[item.key] : fetchEnvironmentsItems[item.key] ?? item.defaultValue; + } + + function setTextValue(item: any, event: React.ChangeEvent) { + setEditedItems({ type: 'EDIT', id: item.key, value: event.target.value }); + } + + function makeToggleFilter(item: any) { + return function (checked: boolean) { + setEditedItems({ type: 'EDIT', id: item.key, value: checked }); + }; + } + + async function submitChanges() { + const response = await UniversimeApi.Group.editEnvironments(editedItems); + refreshPage(); + } + + async function refreshPage() { + const newData = await EnvironmentsFetch(); + setFetchEnvironmentsItems(newData.envDic); + setEditedItems({ type: 'RESET' }); + } +} + diff --git a/src/pages/Settings/EnvironmentsPage/index.ts b/src/pages/Settings/EnvironmentsPage/index.ts new file mode 100644 index 00000000..8b071df1 --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/index.ts @@ -0,0 +1,4 @@ +export { EnvironmentsPage as default } from "./EnvironmentsPage"; + +export * from "./EnvironmentsPage"; +export * from "./EnvironmentsLoader"; diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less new file mode 100644 index 00000000..65b526e5 --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less @@ -0,0 +1,210 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); +@import url(/src/layouts/transitions.less); +@import url(/src/layouts/border-radius.less); + +#email-filter-settings { + .create-new-filter { + width: fit-content; + } + + .email-filter-list { + margin-top: 1rem; + + .email-filter-item { + &:not(:last-of-type) { + @margin-size: .75rem; + + padding-bottom: @margin-size; + margin-bottom: @margin-size; + border-bottom: solid 1px @card-item-color; + } + + .enabled-delete-wrapper { + display: flex; + flex-direction: row; + align-items: end; + justify-content: space-between; + + margin-bottom: .5rem; + + .enabled-wrapper { + display: flex; + flex-direction: row; + align-items: center; + + .filter-enabled-root { + @radix-switch-width: 2.5rem; + @radix-switch-height: 1.5rem; + @radix-thumb-diameter: @radix-switch-height; + + width: @radix-switch-width; + height: @radix-switch-height; + margin-right: .5rem; + + border-radius: 9999px; + border: none; + outline: 2px solid @primary-color; + + box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75); + + cursor: pointer; + + &:focus { + outline: 2px solid @secondary-color; + } + + &:not([data-state='checked']) { + background-color: @card-background-color; + } + + &[data-state='checked'] { + background-color: @card-item-color; + + .filter-enabled-thumb { + transform: translateX(calc(@radix-switch-width - @radix-thumb-diameter)); + background-color: @primary-color; + } + } + + .filter-enabled-thumb { + display: block; + width: @radix-thumb-diameter; + height: @radix-thumb-diameter; + background-color: #FFF; + border-radius: 50%; + outline: 2px solid @primary-color; + + transition: transform 100ms; + will-change: transform; + } + } + } + + .delete-button { + background: transparent; + border: none; + + color: @font-color-v2; + font-size: 1.5rem; + + cursor: pointer; + will-change: color; + transition: @hover-transition-duration color; + + &:hover { + color: @wrong-invalid-color; + } + } + } + + .filter-type-email-wrapper { + @email-font-size: 1rem; + @email-padding: .5rem; + @trigger-width: 9.25em; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + .filter-type-trigger { + background-color: @card-item-color; + + border: none; + border-radius: .625rem; + padding: @email-padding; + width: @trigger-width; + + font-size: @email-font-size; + + cursor: pointer; + + .bi { + margin-left: .5em; + } + + &[data-state="open"] { + .bi::before { + content: "\F286"; + } + } + + &[data-state="closed"] { + .bi::before { + content: "\F282"; + } + } + } + + .filter-type-dropdown-menu { + background-color: @card-item-color; + border: solid 1px @card-background-color; + + border-radius: .625rem; + + overflow: hidden; + + .dropdown-options-item { + padding: .5em 1rem; + font-size: @email-font-size; + + cursor: pointer; + + &:hover { + outline: none; + background-color: @card-background-color; + } + } + } + + .filter-email-input { + font-size: @email-font-size; + padding: @email-padding; + + background-color: @card-item-color; + border: none; + border-radius: .625rem; + + width: calc(100% - @trigger-width - 1em); + } + } + } + } + + .buttons-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + + margin-top: 1rem; + width: 100%; + + > button { + padding: .5em 1em; + border: none; + + font-size: 1rem; + text-transform: uppercase; + font-weight: @font-weight-bold; + + cursor: pointer; + + & + button { + margin-left: 1rem; + } + + &.submit { + color: @font-color-v1; + background-color: @primary-color; + border-radius: .625rem; + + &[disabled] { + background-color: @card-item-color; + color: @font-color-v2; + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts new file mode 100644 index 00000000..c177181b --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts @@ -0,0 +1,28 @@ +import { type LoaderFunctionArgs } from "react-router-dom"; +import { type GroupEmailFilter } from "@/types/Group"; +import UniversimeApi from "@/services/UniversimeApi"; + +export type GroupEmailFilterLoaderResponse = { + emailFilters: GroupEmailFilter[] | undefined; +}; + +export async function GroupEmailFilterFetch(organizationId: string): Promise { + const filters = await UniversimeApi.Group.listEmailFilter({ groupId: organizationId }); + + return { + emailFilters: filters.body?.emailFilters, + } +} + +export async function GroupEmailFilterLoader(args: LoaderFunctionArgs) { + const org = await UniversimeApi.User.organization(); + if (!org.success || !org.body?.organization) { + return FAILED_TO_LOAD; + } + + return GroupEmailFilterFetch(org.body.organization.id); +} + +const FAILED_TO_LOAD = { + emailFilters: undefined, +}; diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx new file mode 100644 index 00000000..c8100a90 --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx @@ -0,0 +1,215 @@ +import { useReducer, useContext } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import * as Switch from "@radix-ui/react-switch" +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import { AuthContext } from "@/contexts/Auth"; +import { SettingsTitle, SettingsDescription } from "@/pages/Settings"; +import { ActionButton } from "@/components/ActionButton/ActionButton"; +import UniversimeApi from "@/services/UniversimeApi"; +import { type OptionInMenu, renderOption } from "@/utils/dropdownMenuUtils"; +import * as SwalUtils from "@/utils/sweetalertUtils"; + +import { type GroupEmailFilterLoaderResponse, GroupEmailFilterFetch } from "./GroupEmailFilterLoader"; +import { GroupEmailFilterTypeToLabel, type GroupEmailFilter, type GroupEmailFilterType } from "@/types/Group"; +import "./GroupEmailFilter.less"; + +let NEW_FILTER_ID = 0; + +export function GroupEmailFilterPage() { + const auth = useContext(AuthContext); + const data = useLoaderData() as GroupEmailFilterLoaderResponse; + const navigate = useNavigate(); + const [emailFilters, emailFiltersDispatch] = useReducer(emailFilterReducer, data.emailFilters); + + if (emailFilters === undefined) { + SwalUtils.fireModal({ + title: "Erro ao recuperar filtros de email", + text: "Não foi possível recuperar os filtros de email", + + showCancelButton: false, + showConfirmButton: true, + confirmButtonText: "Voltar", + }).then(v => navigate("/settings")); + + return null; + } + + const currentEmailFilters = emailFilters.filter(f => f.state !== "DELETED"); + const canSave = undefined !== emailFilters.find(f => f.state !== undefined); + + const FILTER_TYPE_OPTIONS: OptionInMenu[] = Object.keys(GroupEmailFilterTypeToLabel).map(type => ({ + text: GroupEmailFilterTypeToLabel[type as GroupEmailFilterType], + onSelect(data) { + emailFiltersDispatch({ type: "EDIT", filter: { ...data, type: type as GroupEmailFilterType } }); + }, + })); + + + return
+ Filtros de email + Aqui você pode configurar quais emails são permitidos para cadastrar na plataforma. + + + +
+ { currentEmailFilters.length === 0 ?

Nenhum filtro para excluir.

: currentEmailFilters.map(filter => { + return
+
+
+ + + + { filter.enabled ? "Ativado" : "Desativado" } +
+ + +
+ +
+ + + + + + + { FILTER_TYPE_OPTIONS.map(def => renderOption(filter, def)) } + + + + emailFiltersDispatch({type: "EDIT", filter: {...filter, email: e.currentTarget.value}})} /> +
+
+ })} +
+ +
+ +
+
+ + function emailFilterReducer(state: EmailFilterOnList[] | undefined, action: EmailFilterReducerAction): EmailFilterOnList[] | undefined { + if (state === undefined) + return undefined; + + if (action.type === "CREATE") { + return [...state, { added: "", email: "@exemplo.com", enabled: true, id: (--NEW_FILTER_ID).toString(), type: "END_WITH", state: "NEW" }]; + } + + if (action.type === "DELETE") { + const filter = state.find(f => f.id === action.id)!; + + if (filter.state === "NEW") { + return state.filter(f => f.id !== action.id); + } + + else { + return state.map(f => f.id === action.id ? {...f, state: "DELETED"} : f); + } + } + + if (action.type === "EDIT") { + const newFilter: EmailFilterOnList = { + ...action.filter, + state: action.filter.state !== "NEW" ? "EDITED" : "NEW", + } + + return state.map(f => f.id === action.filter.id ? newFilter : f); + } + + if (action.type === "SET") { + return action.value; + } + } + + function makeToggleFilter(filter: EmailFilterOnList) { + return function(checked: boolean) { + emailFiltersDispatch({ + type: "EDIT", + filter: {...filter, enabled: checked}, + }); + } + } + + function makeDeleteFilter(filter: EmailFilterOnList) { + return function() { + SwalUtils.fireModal({ + title: "Deseja deletar esse filtro?", + text: "Essa ação não pode ser desfeita.", + + showCancelButton: true, + cancelButtonText: "Cancelar", + confirmButtonText: "Deletar", + confirmButtonColor: "var(--wrong-invalid-color)" + }).then(response => { + if (response.isConfirmed) { + emailFiltersDispatch({ + type: "DELETE", + id: filter.id, + }); + } + }); + } + } + + async function submitChanges() { + const toCreateFilters = emailFilters!.filter(f => f.state === "NEW"); + const toEditFilters = emailFilters!.filter(f => f.state === "EDITED"); + const toDeleteResponses = emailFilters!.filter(f => f.state === "DELETED"); + + Promise.all([ + Promise.all(toCreateFilters.map(f => UniversimeApi.Group.addEmailFilter({ email: f.email, groupId: auth.organization!.id, isEnabled: f.enabled, type: f.type }))), + Promise.all(toEditFilters.map(f => UniversimeApi.Group.editEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id, email: f.email, isEnabled: f.enabled, type: f.type }))), + Promise.all(toDeleteResponses.map(f => UniversimeApi.Group.deleteEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id }))), + ]).then(([createRes, editRes, deleteRes]) => { + const failedCreate = createRes.filter(f => !f.success); + const failedEdit = editRes.filter(f => !f.success); + const failedDelete = deleteRes.filter(f => !f.success); + + if (failedCreate.length + failedEdit.length + failedDelete.length === 0) { + refreshPage(); + return; + } + + const errorBuilder: string[] = []; + failedCreate.forEach(c => { + errorBuilder.push(`Ao criar filtro: ${c.message}`); + }); + failedEdit.forEach(c => { + errorBuilder.push(`Ao editar filtro: ${c.message}`); + }); + failedDelete.forEach(c => { + errorBuilder.push(`Ao deletar filtro: ${c.message}`); + }); + + refreshPage(); + + SwalUtils.fireModal({ + title: "Erro ao salvar filtros", + html: errorBuilder.join("
"), + icon: "error", + }); + }) + } + + async function refreshPage() { + const newData = await GroupEmailFilterFetch(auth.organization!.id); + + emailFiltersDispatch({ + type: "SET", + value: newData.emailFilters, + }); + } +} + +type EmailFilterOnList = GroupEmailFilter & { state?: "NEW" | "EDITED" | "DELETED"; }; + +type EmailFilterReducerAction = { type: "CREATE"; } | { type: "DELETE"; id: string; } | { type: "EDIT"; filter: EmailFilterOnList; } | { type: "SET", value: EmailFilterOnList[] | undefined }; diff --git a/src/pages/Settings/GroupEmailFilterPage/index.ts b/src/pages/Settings/GroupEmailFilterPage/index.ts new file mode 100644 index 00000000..0a59e69f --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/index.ts @@ -0,0 +1,3 @@ +export { GroupEmailFilterPage as default } from "./GroupEmailFilterPage"; +export * from "./GroupEmailFilterPage"; +export * from "./GroupEmailFilterLoader"; diff --git a/src/pages/Settings/RolesPage/RolesLoader.ts b/src/pages/Settings/RolesPage/RolesLoader.ts new file mode 100644 index 00000000..26c473d9 --- /dev/null +++ b/src/pages/Settings/RolesPage/RolesLoader.ts @@ -0,0 +1,49 @@ +import UniversimeApi from "@/services/UniversimeApi"; +import { LoaderFunctionArgs } from "react-router-dom"; +import { type Profile } from "@/types/Profile"; +import { type Optional } from "@/types/utils"; + +export type RolesPageLoaderResponse = { + success: true, + participants: Profile[]; +} | { + success: false, + reason: Optional, +}; + +export async function RolesPageFetch(organizationId: string): Promise { + const participants = await UniversimeApi.Group.participants({ groupId: organizationId }); + + if (!participants.success || !participants.body) { + return { + success: false, + reason: participants.message, + }; + } + + // if doesn't have access to accessLevel = not an admin + if (participants.body.participants.find(p => p.user.accessLevel === undefined)) { + return { + success: false, + reason: undefined, + }; + } + + return { + success: true, + participants: participants.body.participants, + }; +} + +export async function RolesPageLoader(args: LoaderFunctionArgs): Promise { + const organization = await UniversimeApi.User.organization(); + + if (!organization.success || !organization.body?.organization) { + return { + success: false, + reason: organization.message, + }; + } + + return RolesPageFetch(organization.body.organization.id); +} diff --git a/src/pages/Settings/RolesPage/RolesPage.less b/src/pages/Settings/RolesPage/RolesPage.less new file mode 100644 index 00000000..69e8d5df --- /dev/null +++ b/src/pages/Settings/RolesPage/RolesPage.less @@ -0,0 +1,140 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); + +#roles-settings { + #search-submit-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + width: 100%; + margin-bottom: 1rem; + + font-size: 1rem; + + @submit-width: 11em; + @padding: .75em .25em; + @search-width: calc(100% - 1em - @submit-width); + + input[type="search"] { + padding: @padding; + border-radius: .625rem; + border: none; + + width: @search-width; + + &:focus { + outline-color: @primary-color; + } + } + + .submit { + padding: @padding; + border-radius: .625rem; + border: none; + + width: @submit-width; + + background-color: @primary-color; + + color: @font-color-v1; + text-transform: uppercase; + font-weight: @font-weight-bold; + + cursor: pointer; + + &:disabled { + background-color: @card-item-color; + color: @font-color-v2; + cursor: default; + } + } + } + + #participants-list { + .profile-item { + display: flex; + flex-direction: row; + align-items: start; + + @image-width: 7.5rem; + @set-role-width: 9.5rem; + @role-font-size: 1rem; + + &:not(:last-child) { + margin-bottom: 1.5rem; + } + + .profile-image { + width: @image-width; + } + + .info { + @margin: .5rem; + margin-top: @margin; + margin-bottom: @margin; + margin-left: @margin; + margin-right: 1rem; + } + + .set-role-trigger { + width: @set-role-width; + font-size: @role-font-size; + + margin-top: .5em; + margin-left: auto; + + border: none; + border-radius: .625rem; + background-color: @card-item-color; + padding: .5em; + + cursor: pointer; + + &:disabled { + cursor: default; + } + + .bi { + margin-left: .5em; + } + + &[data-state="open"] .bi::before { + content: "\F286"; + } + + &[data-state="closed"] .bi::before { + content: "\F282"; + } + } + + .set-role-menu { + width: @set-role-width; + font-size: @role-font-size; + + background-color: @card-item-color; + border-radius: .625rem; + border: solid 2px @card-background-color; + + display: flex; + flex-direction: column; + align-items: center; + + overflow: hidden; + + .dropdown-options-item { + width: @set-role-width; + text-align: center; + + padding: .75em 0; + cursor: pointer; + + &:hover{ + background-color: @card-background-color; + } + } + } + } + } +} diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx new file mode 100644 index 00000000..3991ce8f --- /dev/null +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -0,0 +1,166 @@ +import { useReducer, useState, useContext } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import UniversimeApi from "@/services/UniversimeApi"; +import { AuthContext } from "@/contexts/Auth"; +import { SettingsTitle, type RolesPageLoaderResponse, RolesPageFetch, SettingsDescription } from "@/pages/Settings"; +import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; +import { setStateAsValue } from "@/utils/tsxUtils"; +import { getFullName, getProfileImageUrl } from "@/utils/profileUtils"; +import { type OptionInMenu, renderOption } from "@/utils/dropdownMenuUtils"; +import * as SwalUtils from "@/utils/sweetalertUtils"; + +import { type Profile } from "@/types/Profile"; +import { UserAccessLevelLabel, type UserAccessLevel, compareAccessLevel } from "@/types/User"; +import { type Optional } from "@/types/utils"; +import "./RolesPage.less"; + +export function RolesPage() { + const data = useLoaderData() as RolesPageLoaderResponse; + const navigate = useNavigate(); + const auth = useContext(AuthContext); + + const [participants, participantsDispatch] = useReducer(participantsReducer, data.success ? data.participants : undefined); + const [filter, setFilter] = useState(""); + + if (!participants) { + SwalUtils.fireModal({ + title: "Erro ao recuperar dados dos participantes", + text: data.success === false ? data.reason : undefined, + + showConfirmButton: true, + confirmButtonText: "Voltar", + }).then(res => navigate("/settings")); + return null; + } + + const filteredParticipants = participants + .filter(p => p.firstname?.toLocaleLowerCase().includes(filter.toLocaleLowerCase())) + .sort((a, b) => { + if (a.user.accessLevel !== b.user.accessLevel) { + return compareAccessLevel(a.user.accessLevel!, b.user.accessLevel!); + } + + return getFullName(a).localeCompare(getFullName(b)); + }); + + const CHANGE_ROLE_OPTIONS: OptionInMenu[] = Object.entries(UserAccessLevelLabel).map(([role, label]) => ({ + text: label, + onSelect(data) { + participantsDispatch({ + type: "SET_ROLE", + profileId: data.id, + setRole: role as UserAccessLevel, + }); + }, + })); + + const canSubmit = participants.filter(p => p.changed).length > 0; + + return
+ Configurar administradores + Configure os níveis de acesso dos usuários do Universi.me +
+ + +
+ +
+ { filteredParticipants.map(profile => { + const isOwnProfile = auth.profile!.id === profile.id; + + return
+ +
+

{getFullName(profile)}

+

{profile.bio}

+
+ + + + + + + { CHANGE_ROLE_OPTIONS.map(def => renderOption(profile, def)) } + + +
+ }) } +
+
+ + function participantsReducer(state: Optional, action: ParticipantsReducerAction): Optional { + if (state === undefined || data.success === false) + return undefined; + + if (action.type === "SET_ALL") { + return action.setParticipants; + } + + return state.map(p => { + if (p.id !== action.profileId) + return p; + + const unchanging = data.participants + .find(p => p.id === action.profileId)! + .user.accessLevel === action.setRole; + + return { + ...p, + changed: unchanging ? undefined : true, + user: { + ...p.user, + accessLevel: action.setRole, + } + } + }); + } + + async function refreshPage() { + const response = await RolesPageFetch(auth.organization!.id); + participantsDispatch({ + type: "SET_ALL", + setParticipants: response.success ? response.participants : undefined, + }); + } + + async function submitChanges() { + if (!participants) + return; + + const toUpdate = participants.filter(p => p.changed); + const responses = await Promise.all( + toUpdate.map(p => UniversimeApi.Admin.editAccount({ userId: p.user.id, authorityLevel: p.user.accessLevel })) + ); + + refreshPage(); + + const failedChanges = responses.filter(r => !r.success); + if (failedChanges.length === 0) + return; + + const messages = failedChanges + .map(err => err.message) + .filter(m => m !== undefined); + + SwalUtils.fireModal({ + title: "Erro ao alterar administradores", + text: messages.join("\n"), + }); + } +} + +type ProfileOnList = Profile & { changed?: true }; + +type ParticipantsReducerAction = { + type: "SET_ROLE"; + setRole: UserAccessLevel; + profileId: string; +} | { + type: "SET_ALL"; + setParticipants: Optional; +}; diff --git a/src/pages/Settings/RolesPage/index.ts b/src/pages/Settings/RolesPage/index.ts new file mode 100644 index 00000000..1d4afbc8 --- /dev/null +++ b/src/pages/Settings/RolesPage/index.ts @@ -0,0 +1,4 @@ +export { RolesPage as default } from "./RolesPage"; + +export * from "./RolesPage"; +export * from "./RolesLoader"; diff --git a/src/pages/Settings/Settings.less b/src/pages/Settings/Settings.less new file mode 100644 index 00000000..c5d44ff4 --- /dev/null +++ b/src/pages/Settings/Settings.less @@ -0,0 +1,97 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); +@import url(/src/layouts/border-radius.less); +@import url(/src/layouts/spacing.less); + +#universi-settings { + width: 100%; + + display: flex; + align-items: center; + justify-content: center; + + #settings-content { + min-width: 500px; + max-width: min(75%, 1024px); + + margin: @top-page-margin @left-page-padding; + padding: 1rem 1rem; + + border-radius: @border-radius; + background-color: @card-background-color; + border: solid 1px @card-background-color; + + color: @font-color-v2; + + overflow: hidden; + + @option-padding-x: 2rem; + + .settings-page-title { + font-size: 1.5rem; + font-weight: @font-weight-bold; + margin-bottom: .25rem; + + a { + color: inherit; + margin-right: .5em; + } + } + + .settings-description { + margin-bottom: .5rem; + } + + > .settings-option { + display: flex; + flex-direction: column; + align-items: center; + + background-color: transparent; + + color: inherit; + text-decoration: none; + + cursor: pointer; + + will-change: background-color; + + &:hover { + background-color: @card-item-color; + } + + &:not(:last-child)::after { + content: ""; + background-color: @card-item-color; + width: 100%; + height: 1px; + } + + .settings-option-content { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + padding: 1rem @option-padding-x; + width: 100%; + + .settings-option-info { + .settings-option-title { + font-weight: @font-weight-bold; + font-size: 1.25rem; + } + + .settings-option-description { + font-weight: @font-weight-default; + } + } + + .bi { + margin-left: 2rem; + font-size: 1.5em; + } + } + } + } +} diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx new file mode 100644 index 00000000..306f5597 --- /dev/null +++ b/src/pages/Settings/Settings.tsx @@ -0,0 +1,30 @@ +import { useContext } from "react"; +import { useOutlet } from "react-router"; +import { Navigate } from "react-router-dom"; + +import { SettingsTitle, SettingsMoveTo } from "@/pages/Settings"; +import { AuthContext } from "@/contexts/Auth"; + +import "./Settings.less"; + +export function SettingsPage() { + const outlet = useOutlet(); + const auth = useContext(AuthContext); + + if (auth.profile?.user.accessLevel !== "ROLE_ADMIN") + return ; + + return
+
+ { outlet ?? <> + Configurações + + + + + + + } +
+
+} diff --git a/src/pages/Settings/SettingsDescription/SettingsDescription.tsx b/src/pages/Settings/SettingsDescription/SettingsDescription.tsx new file mode 100644 index 00000000..64df19b0 --- /dev/null +++ b/src/pages/Settings/SettingsDescription/SettingsDescription.tsx @@ -0,0 +1,7 @@ +import { PropsWithChildren } from "react"; + +export type SettingsDescriptionProps = PropsWithChildren<{}>; + +export function SettingsDescription(props: SettingsDescriptionProps) { + return

{ props.children }

+} diff --git a/src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx b/src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx new file mode 100644 index 00000000..ddf82d90 --- /dev/null +++ b/src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from "react"; +import { Link } from "react-router-dom"; + +export type SettingsMoveToProps = { + title: NonNullable; + description?: ReactNode; + to: string; +}; + +export function SettingsMoveTo(props: Readonly) { + return +
+
+

{ props.title }

+ { props.description &&

{ props.description }

} +
+ + +
+ +} diff --git a/src/pages/Settings/SettingsTitle/SettingsTitle.tsx b/src/pages/Settings/SettingsTitle/SettingsTitle.tsx new file mode 100644 index 00000000..786324b8 --- /dev/null +++ b/src/pages/Settings/SettingsTitle/SettingsTitle.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from "react"; +import { Link } from "react-router-dom"; + +export type SettingsTitleProps = { + children: NonNullable; +}; + +export function SettingsTitle({ children }: Readonly) { + return
+ { location.pathname !== "/settings" && + + } + { children } +
+} diff --git a/src/pages/Settings/index.ts b/src/pages/Settings/index.ts new file mode 100644 index 00000000..9b9ef815 --- /dev/null +++ b/src/pages/Settings/index.ts @@ -0,0 +1,9 @@ +export { SettingsPage as default } from "./Settings"; + +export * from "./Settings"; +export * from "./SettingsTitle/SettingsTitle"; +export * from "./SettingsDescription/SettingsDescription"; +export * from "./SettingsOptions/SettingsMoveTo"; +export * from "./GroupEmailFilterPage"; +export * from "./RolesPage"; +export * from "./EnvironmentsPage"; diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.less b/src/pages/SignUp/SignUpModal/SignUpModal.less index 73dd0549..8db2c07b 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.less +++ b/src/pages/SignUp/SignUpModal/SignUpModal.less @@ -85,6 +85,37 @@ font-weight: @font-weight-semibold; } + &#fieldset-name { + display: flex; + flex-direction: column; + + + label { + display: flex; + flex-direction: column; + + &:not(:last-of-type) { + margin-bottom: 1rem; + } + + input { + width: 100%; + } + } + + .counter-wrapper { + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: end; + + .counter { + font-size: .75em; + } + } + } + &.invalid-email { input[name="email"] { background-color: #F5E0E0; diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.tsx b/src/pages/SignUp/SignUpModal/SignUpModal.tsx index b5118693..a294a726 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.tsx +++ b/src/pages/SignUp/SignUpModal/SignUpModal.tsx @@ -1,10 +1,12 @@ -import { MouseEvent, FocusEvent, useState, useEffect } from "react"; +import { MouseEvent, FocusEvent, useState, useEffect, useContext } from "react"; import { useNavigate } from "react-router"; import ReCAPTCHA from "react-google-recaptcha-enterprise"; import { UniversiModal } from "@/components/UniversiModal"; import UniversimeApi from "@/services/UniversimeApi"; import { isEmail } from "@/utils/regexUtils"; +import { setStateAsValue } from "@/utils/tsxUtils"; +import { AuthContext } from "@/contexts/Auth/AuthContext"; import { minimumLength, numberOrSpecialChar, passwordValidationClass, upperAndLowerCase } from "@/utils/passwordValidation"; import { enableSignUp } from "./helperFunctions"; import * as SwalUtils from "@/utils/sweetalertUtils"; @@ -15,12 +17,17 @@ export type SignUpModalProps = { toggleModal: (state: boolean) => any; }; +const FIRST_NAME_MAX_LENGTH = 21; +const LAST_NAME_MAX_LENGTH = 21; const USERNAME_CHAR_REGEX = /[a-z0-9_.-]/ export function SignUpModal(props: SignUpModalProps) { + const auth = useContext(AuthContext); const navigate = useNavigate(); + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -36,6 +43,8 @@ export function SignUpModal(props: SignUpModalProps) { const [recaptchaToken, setRecaptchaToken] = useState(null); const [recaptchaRef, setRecaptchaRef] = useState(null); + const isFirstnameFull = (firstname.length) >= FIRST_NAME_MAX_LENGTH; + const isLastnameFull = (lastname.length) >= LAST_NAME_MAX_LENGTH; const canSignUp = enableSignUp(username, email, password); @@ -74,7 +83,9 @@ export function SignUpModal(props: SignUpModalProps) { return () => clearTimeout(delayDebounceFn) }, [email]) - const ENABLE_RECAPTCHA = import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"; + const organizationEnv = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any); + const ENABLE_RECAPTCHA = organizationEnv.recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = organizationEnv.recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return ( @@ -88,6 +99,24 @@ export function SignUpModal(props: SignUpModalProps) {
+
+ + + +
Email
- setRecaptchaRef(r) } sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} /> + setRecaptchaRef(r) } sitekey={RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} />
} @@ -166,7 +195,7 @@ export function SignUpModal(props: SignUpModalProps) { function createAccount(e: MouseEvent) { e.preventDefault(); - UniversimeApi.User.signUp({ username, email, password, recaptchaToken }) + UniversimeApi.User.signUp({ firstname, lastname, username, email, password, recaptchaToken }) .then(res => { if (!res.success) { recaptchaRef.reset(); diff --git a/src/pages/singin/SinginForm.tsx b/src/pages/singin/SinginForm.tsx index 108812b9..691c398b 100644 --- a/src/pages/singin/SinginForm.tsx +++ b/src/pages/singin/SinginForm.tsx @@ -38,9 +38,12 @@ export default function SinginForm() { setShowPassword(!showPassword); }; - const ENABLE_GOOGLE_LOGIN = import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "true" || import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "1"; - const ENABLE_RECAPTCHA = import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"; - + const organizationEnv = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any); + const SIGNUP_ENABLED = organizationEnv.signup_enabled ?? true; + const ENABLE_GOOGLE_LOGIN = organizationEnv.login_google_enabled ?? (import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "true" || import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "1"); + const ENABLE_RECAPTCHA = organizationEnv.recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = organizationEnv.recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; + return ( <> @@ -85,7 +88,7 @@ export default function SinginForm() { !ENABLE_RECAPTCHA ? null :

- setRecaptchaRef(r) } sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} /> + setRecaptchaRef(r) } sitekey={RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} />
} @@ -121,10 +124,11 @@ export default function SinginForm() { } - + { !SIGNUP_ENABLED ? null :
Crie sua conta
+ }
Esqueci minha senha
diff --git a/src/services/UniversimeApi/Admin.ts b/src/services/UniversimeApi/Admin.ts new file mode 100644 index 00000000..b57fcb72 --- /dev/null +++ b/src/services/UniversimeApi/Admin.ts @@ -0,0 +1,44 @@ +import { api } from "./api"; +import { type ApiResponse } from "@/types/UniversimeApi"; +import { type User, type UserAccessLevel } from "@/types/User"; + +export type AdminEditAccount_RequestDTO = { + userId: string; + username?: string; + email?: string; + password?: string; + authorityLevel?: UserAccessLevel; + emailVerified?: boolean; + blockedAccount?: boolean; + inactiveAccount?: boolean; + credentialsExpired?: boolean; + expiredUser?: boolean; +}; + +export type AdminListAccounts_RequestDTO = { + accessLevel?: UserAccessLevel; +}; + +export type AdminEditAccount_ResponseDTO = ApiResponse; +export type AdminListAccounts_ResponseDTO = ApiResponse<{ users: User[] }>; + +export async function editAccount(body: AdminEditAccount_RequestDTO) { + return (await api.post("/admin/account/edit", { + userId: body.userId, + username: body.username, + email: body.email, + password: body.password, + authorityLevel: body.authorityLevel, + emailVerified: body.emailVerified, + blockedAccount: body.blockedAccount, + inactiveAccount: body.inactiveAccount, + credentialsExpired: body.credentialsExpired, + expiredUser: body.expiredUser, + })).data; +} + +export async function listAccounts(body: AdminListAccounts_RequestDTO) { + return (await api.post("/admin/account/list", { + accessLevel: body.accessLevel, + })).data; +} diff --git a/src/services/UniversimeApi/Group.ts b/src/services/UniversimeApi/Group.ts index 83386c1a..ff72af0d 100644 --- a/src/services/UniversimeApi/Group.ts +++ b/src/services/UniversimeApi/Group.ts @@ -1,4 +1,4 @@ -import type { Group, GroupType } from "@/types/Group"; +import type { Group, GroupType, GroupEmailFilter, GroupEmailFilterType } from "@/types/Group"; import type { Profile } from "@/types/Profile"; import type { ApiResponse } from "@/types/UniversimeApi"; import { api } from "./api"; @@ -39,15 +39,36 @@ export type GroupIdOrPath_RequestDTO = { groupPath?: string; }; -export type GroupGet_ResponseDTO = ApiResponse<{ group: Group }>; -export type GroupCreate_ResponseDTO = ApiResponse; -export type GroupUpdate_ResponseDTO = ApiResponse; -export type GroupAvailableParents_ResponseDTO = ApiResponse<{ groups: Group[] }>; -export type GroupSubgroups_ResponseDTO = ApiResponse<{ subgroups: Group[] }>; -export type GroupParticipants_ResponseDTO = ApiResponse<{ participants: Profile[] }>; -export type GroupJoin_ResponseDTO = ApiResponse; -export type GroupExit_ResponseDTO = ApiResponse; -export type GroupFolders_ResponseDTO = ApiResponse<{ folders: Folder[] }>; +export type GroupEmailFilterAdd_RequestDTO = GroupIdOrPath_RequestDTO & { + email: string; + isEnabled?: boolean; + type?: GroupEmailFilterType; +}; + +export type GroupEmailFilterEdit_RequestDTO = GroupIdOrPath_RequestDTO & { + emailFilterId: string; + email?: string; + isEnabled?: boolean; + type?: GroupEmailFilterType; +}; + +export type GroupEmailFilterDelete_RequestDTO = GroupIdOrPath_RequestDTO & { + emailFilterId: string; +}; + +export type GroupGet_ResponseDTO = ApiResponse<{ group: Group }>; +export type GroupCreate_ResponseDTO = ApiResponse; +export type GroupUpdate_ResponseDTO = ApiResponse; +export type GroupAvailableParents_ResponseDTO = ApiResponse<{ groups: Group[] }>; +export type GroupSubgroups_ResponseDTO = ApiResponse<{ subgroups: Group[] }>; +export type GroupParticipants_ResponseDTO = ApiResponse<{ participants: Profile[] }>; +export type GroupJoin_ResponseDTO = ApiResponse; +export type GroupExit_ResponseDTO = ApiResponse; +export type GroupFolders_ResponseDTO = ApiResponse<{ folders: Folder[] }>; +export type GroupEmailFilterAdd_ResponseDTO = ApiResponse; +export type GroupEmailFilterEdit_ResponseDTO = ApiResponse; +export type GroupEmailFilterDelete_ResponseDTO = ApiResponse; +export type GroupEmailFilterList_ResponseDTO = ApiResponse<{ emailFilters: GroupEmailFilter[] }>; export async function get(body: GroupIdOrPath_RequestDTO) { return (await api.post('/group/get', { @@ -119,3 +140,47 @@ export async function folders(body: GroupIdOrPath_RequestDTO) { groupPath: body.groupPath, })).data; } + +export async function addEmailFilter(body: GroupEmailFilterAdd_RequestDTO) { + return (await api.post("/group/settings/email-filter/add", { + groupId: body.groupId, + groupPath: body.groupPath, + email: body.email, + enabled: body.isEnabled, + type: body.type, + })).data; +} + +export async function editEmailFilter(body: GroupEmailFilterEdit_RequestDTO) { + return (await api.post("/group/settings/email-filter/edit", { + groupId: body.groupId, + groupPath: body.groupPath, + groupEmailFilterId: body.emailFilterId, + email: body.email, + enabled: body.isEnabled, + type: body.type, + })).data; +} + +export async function deleteEmailFilter(body: GroupEmailFilterDelete_RequestDTO) { + return (await api.post("/group/settings/email-filter/delete", { + groupId: body.groupId, + groupPath: body.groupPath, + groupEmailFilterId: body.emailFilterId, + })).data; +} + +export async function listEmailFilter(body: GroupIdOrPath_RequestDTO) { + return (await api.post("/group/settings/email-filter/list", { + groupId: body.groupId, + groupPath: body.groupPath, + })).data; +} + +export async function editEnvironments(body: {}) { + return (await api.post("/group/settings/environments/edit", body)).data; +} + +export async function listEnvironments(body: {}) { + return (await api.post("/group/settings/environments/list", body)).data; +} diff --git a/src/services/UniversimeApi/Profile.ts b/src/services/UniversimeApi/Profile.ts index 216e8289..f8060ddf 100644 --- a/src/services/UniversimeApi/Profile.ts +++ b/src/services/UniversimeApi/Profile.ts @@ -14,6 +14,7 @@ export type ProfileEdit_RequestDTO = { bio?: string; gender?: string; imageUrl?: string; + rawPassword?: string; }; export type ProfileIdAndUsername_RequestDTO = { diff --git a/src/services/UniversimeApi/User.ts b/src/services/UniversimeApi/User.ts index f43afd18..33cbd6ae 100644 --- a/src/services/UniversimeApi/User.ts +++ b/src/services/UniversimeApi/User.ts @@ -3,6 +3,8 @@ import { api } from "./api"; import { Group } from "@/types/Group"; export type UserSignUp_RequestDTO = { + firstname: string; + lastname: string; username: string; email: string; password: string; @@ -38,6 +40,8 @@ export type UserOrganization_ResponseDTO = ApiResponse<{organization : Group | n export async function signUp(body: UserSignUp_RequestDTO) { return (await api.post("/signup", { + firstname: body.firstname, + lastname: body.lastname, username: body.username, email: body.email, password: body.password, diff --git a/src/services/UniversimeApi/index.ts b/src/services/UniversimeApi/index.ts index f10d80a0..2092176b 100644 --- a/src/services/UniversimeApi/index.ts +++ b/src/services/UniversimeApi/index.ts @@ -8,6 +8,7 @@ import * as Profile from "./Profile"; import * as Capacity from "./Capacity" import * as User from "./User" import * as Image from "./Image" +import * as Admin from "./Admin" export const UniversimeApi = { api, @@ -20,6 +21,7 @@ export const UniversimeApi = { Profile, User, Image, + Admin, }; export default UniversimeApi; diff --git a/src/services/routes.tsx b/src/services/routes.tsx index b2ec19e4..2113330c 100644 --- a/src/services/routes.tsx +++ b/src/services/routes.tsx @@ -20,6 +20,7 @@ import Recovery from "@/pages/Recovery/Recovery"; import NewPassword from "@/pages/NewPassword/NewPassword"; import ManageProfilePage, { ManageProfileLoader } from "@/pages/ManageProfile"; import Homepage from "@/pages/Homepage"; +import SettingsPage, { GroupEmailFilterPage, GroupEmailFilterLoader, RolesPage, RolesPageLoader, EnvironmentsPage, EnvironmentsLoader } from "@/pages/Settings"; @@ -96,6 +97,27 @@ export const router = createBrowserRouter([{ element: , loader: ManageGroupLoader, }, + { + path: "/settings", + element: , + children: [ + { + path: "email-filter", + element: , + loader: GroupEmailFilterLoader, + }, + { + path: "roles", + element: , + loader: RolesPageLoader, + }, + { + path: "environments", + element: , + loader: EnvironmentsLoader, + } + ], + } ] }, ]) diff --git a/src/types/Group.ts b/src/types/Group.ts index 6ce43a97..3eb94ad5 100644 --- a/src/types/Group.ts +++ b/src/types/Group.ts @@ -17,6 +17,26 @@ export type Group = { rootGroup: boolean; bannerImage: string | null; organization: Group | null; + canEdit: boolean; +}; + +export type GroupEmailFilter = { + id: string; + enabled: boolean; + type: GroupEmailFilterType; + email: string; + added: string; +}; + +export type GroupEmailFilterType = "END_WITH" | "START_WITH" | "CONTAINS" | "EQUALS" | "MASK" | "REGEX"; + +export const GroupEmailFilterTypeToLabel = { + "END_WITH": "Terminando em", + "START_WITH": "Começando com", + "CONTAINS": "Contendo", + "EQUALS": "Igual a", + "MASK": "Máscara ( * )", + "REGEX": "Padrão RegEx", }; export type GroupType = "INSTITUTION" | "CAMPUS" | "COURSE" | "PROJECT" | "CLASSROOM" | "MONITORIA" | "LABORATORY" diff --git a/src/types/User.ts b/src/types/User.ts index 92be501d..9a2d4650 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -1,7 +1,44 @@ +export type UserAccessLevel = "ROLE_USER" | "ROLE_DEV" | "ROLE_ADMIN"; + export type User = { id: string; name: string; email?: string; ownerOfSession: boolean; needProfile: boolean; + accessLevel?: UserAccessLevel; +} + +export const UserAccessLevelLabel: { [k in UserAccessLevel]: string } = { + ROLE_ADMIN: "Administrador", + ROLE_DEV: "Desenvolvedor", + ROLE_USER: "Usuário", +} + +export function compareAccessLevel(a: UserAccessLevel, b: UserAccessLevel): number { + const A_FIRST = -1; + const KEEP_ORDER = 0; + const B_FIRST = 1; + + if (a === b) { + return KEEP_ORDER; + } + + if (a === "ROLE_ADMIN") { + return A_FIRST; + } + + if (b === "ROLE_ADMIN") { + return B_FIRST; + } + + if (a === "ROLE_DEV") { + return A_FIRST; + } + + if (b === "ROLE_DEV") { + return B_FIRST; + } + + return KEEP_ORDER; } diff --git a/src/types/utils.ts b/src/types/utils.ts index 4aa98cda..32471f22 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -1 +1,3 @@ -export type NullableBoolean = null | boolean; +export type Nullable = null | T; +export type NullableBoolean = Nullable; +export type Optional = T | undefined; diff --git a/src/utils/profileUtils.ts b/src/utils/profileUtils.ts index 2879219d..6c792698 100644 --- a/src/utils/profileUtils.ts +++ b/src/utils/profileUtils.ts @@ -30,7 +30,11 @@ export function getGenderName(gender: Gender | null | undefined): string { } export function getProfileImageUrl(profile: Profile): string | null { - return profile.image && profile.image.startsWith("/") + if (!profile.image) { + return "/assets/imgs/default_avatar.png"; + } + + return profile.image.startsWith("/") ? `${import.meta.env.VITE_UNIVERSIME_API}${profile.image}` : profile.image; }