diff --git a/backend/src/api/tenant/index.ts b/backend/src/api/tenant/index.ts index 0ea84da5d2..97096f37d9 100644 --- a/backend/src/api/tenant/index.ts +++ b/backend/src/api/tenant/index.ts @@ -15,6 +15,7 @@ export default (app) => { app.get(`/tenant`, safeWrap(require('./tenantList').default)) app.get(`/tenant/url`, safeWrap(require('./tenantFind').default)) app.get(`/tenant/:id`, safeWrap(require('./tenantFind').default)) + app.get(`/tenant/:id/name`, safeWrap(require('./tenantFindName').default)) app.get(`/tenant/:tenantId/membersToMerge`, safeWrap(require('./tenantMembersToMerge').default)) app.get( `/tenant/:tenantId/organizationsToMerge`, diff --git a/backend/src/api/tenant/tenantFind.ts b/backend/src/api/tenant/tenantFind.ts index 6f27d6c1b2..6670237626 100644 --- a/backend/src/api/tenant/tenantFind.ts +++ b/backend/src/api/tenant/tenantFind.ts @@ -1,10 +1,13 @@ import identifyTenant from '../../segment/identifyTenant' import TenantService from '../../services/tenantService' import Error404 from '../../errors/Error404' +import PermissionChecker from '../../services/user/permissionChecker' +import Permissions from '../../security/permissions' export default async (req, res) => { + req.currentTenant = { id: req.params.id } + new PermissionChecker(req).validateHas(Permissions.values.memberRead) let payload - if (req.params.id) { payload = await new TenantService(req).findById(req.params.id) } else { diff --git a/backend/src/api/tenant/tenantFindName.ts b/backend/src/api/tenant/tenantFindName.ts new file mode 100644 index 0000000000..ae25895943 --- /dev/null +++ b/backend/src/api/tenant/tenantFindName.ts @@ -0,0 +1,23 @@ +import identifyTenant from '../../segment/identifyTenant' +import TenantService from '../../services/tenantService' +import Error404 from '../../errors/Error404' + +export default async (req, res) => { + // This endpoint is unauthenticated on purpose, but public reprots. + const payload = await new TenantService(req).findById(req.params.id) + + if (payload) { + if (req.currentUser) { + identifyTenant({ ...req, currentTenant: payload }) + } + + const payloadOut = { + name: payload.name, + id: payload.id, + } + + await req.responseHandler.success(req, res, payloadOut) + } else { + throw new Error404() + } +} diff --git a/backend/src/database/repositories/userRepository.ts b/backend/src/database/repositories/userRepository.ts index 7f1f7a63c1..afcd43ce8e 100644 --- a/backend/src/database/repositories/userRepository.ts +++ b/backend/src/database/repositories/userRepository.ts @@ -547,12 +547,22 @@ export default class UserRepository { status: 'active', }, }) + record = { ...record, ...record.json, } delete record.json + // Remove sensitive fields + delete record.password + delete record.emailVerificationToken + delete record.emailVerificationTokenExpiresAt + delete record.providerId + delete record.passwordResetToken + delete record.passwordResetTokenExpiresAt + delete record.jwtTokenInvalidBefore + if (!record) { throw new Error404() } diff --git a/backend/src/i18n/en.ts b/backend/src/i18n/en.ts index 413123012c..86874dbe80 100644 --- a/backend/src/i18n/en.ts +++ b/backend/src/i18n/en.ts @@ -45,6 +45,8 @@ const en = { invalidToken: 'Invalid or expired password reset link', error: `Invalid email`, }, + passwordInvalid: + 'Passwords must have at least one letter, one number, one symbol, and be at least 8 characters long.', emailAddressVerificationEmail: { invalidToken: 'Invalid or expired email verification link.', error: `Email not recognized.`, diff --git a/backend/src/security/permissions.ts b/backend/src/security/permissions.ts index f701f4ab02..7e0ceced70 100644 --- a/backend/src/security/permissions.ts +++ b/backend/src/security/permissions.ts @@ -99,7 +99,7 @@ class Permissions { }, userRead: { id: 'userRead', - allowedRoles: [roles.admin, roles.readonly], + allowedRoles: [roles.admin], allowedPlans: [ plans.essential, plans.growth, @@ -110,7 +110,7 @@ class Permissions { }, userAutocomplete: { id: 'userAutocomplete', - allowedRoles: [roles.admin, roles.readonly], + allowedRoles: [roles.admin], allowedPlans: [ plans.essential, plans.growth, diff --git a/backend/src/services/auth/authService.ts b/backend/src/services/auth/authService.ts index e72f19acac..e18fb9695c 100644 --- a/backend/src/services/auth/authService.ts +++ b/backend/src/services/auth/authService.ts @@ -38,6 +38,12 @@ class AuthService { const existingUser = await UserRepository.findByEmail(email, options) + const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/ + + if (!passwordRegex.test(password)) { + throw new Error400(options.language, 'auth.passwordInvalid') + } + // Generates a hashed password to hide the original one. const hashedPassword = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS) diff --git a/frontend/src/i18n/en.js b/frontend/src/i18n/en.js index 36f3a2c648..cca01dcbe2 100644 --- a/frontend/src/i18n/en.js +++ b/frontend/src/i18n/en.js @@ -813,7 +813,7 @@ const en = { /* eslint-disable */ validation: { mixed: { - default: 'path} is invalid', + default: '{path} is invalid', required: 'This field is required', oneOf: '{path} must be one of the following values: ${values}', diff --git a/frontend/src/modules/auth/pages/signin-page.vue b/frontend/src/modules/auth/pages/signin-page.vue index bb4bfac6c9..e480dd0c82 100644 --- a/frontend/src/modules/auth/pages/signin-page.vue +++ b/frontend/src/modules/auth/pages/signin-page.vue @@ -72,6 +72,11 @@ class="h-4 flex items-center ri-error-warning-line text-base text-red-500" /> Passwords must have at least one letter, one number, one symbol, and be at least 8 characters long. + {{ error }} diff --git a/frontend/src/modules/auth/pages/signup-page.vue b/frontend/src/modules/auth/pages/signup-page.vue index eca7056e09..2a1170097a 100644 --- a/frontend/src/modules/auth/pages/signup-page.vue +++ b/frontend/src/modules/auth/pages/signup-page.vue @@ -129,6 +129,11 @@ class="h-4 flex items-center ri-error-warning-line text-base text-red-500" /> Passwords must have at least one letter, one number, one symbol, and be at least 8 characters long. + {{ error }} diff --git a/frontend/src/modules/layout/components/menu/workspace/menu-workspace-popover.vue b/frontend/src/modules/layout/components/menu/workspace/menu-workspace-popover.vue index 337090fb83..e8c7331c9b 100644 --- a/frontend/src/modules/layout/components/menu/workspace/menu-workspace-popover.vue +++ b/frontend/src/modules/layout/components/menu/workspace/menu-workspace-popover.vue @@ -15,7 +15,7 @@ :disable-active-class="true" link-class="!p-3 !h-10 !mb-0 !mt-1 !text-xs" /> -
+
Edit workspace
@@ -25,8 +25,14 @@ -
+
@@ -102,6 +108,7 @@ import { computed } from 'vue'; import { useUserStore } from '@/modules/user/store/pinia'; import { storeToRefs } from 'pinia'; import { FeatureFlag } from '@/utils/featureFlag'; +import { SettingsPermissions } from '@/modules/settings/settings-permissions'; const emit = defineEmits<{(e:'add'): any, (e: 'edit', value: TenantModel): any}>(); @@ -113,6 +120,17 @@ const userStore = useUserStore(); const { isDeveloperModeActive } = storeToRefs(userStore); const { updateDeveloperMode } = userStore; +const hasPermissionsForSettings = computed( + () => { + const settingsPermissions = new SettingsPermissions( + currentTenant.value, + currentUser.value, + ); + + return settingsPermissions.edit || settingsPermissions.lockedForCurrentPlan; + }, +); + const tenants = computed(() => { const currentTenantId = currentTenant.value.id; const restTenants = rows.value.filter((ten: TenantModel) => ten.id !== currentTenantId) diff --git a/frontend/src/modules/layout/config/links/api-keys.ts b/frontend/src/modules/layout/config/links/api-keys.ts index 771c139be4..5cb155206d 100644 --- a/frontend/src/modules/layout/config/links/api-keys.ts +++ b/frontend/src/modules/layout/config/links/api-keys.ts @@ -1,4 +1,5 @@ import { MenuLink } from '@/modules/layout/types/MenuLink'; +import { SettingsPermissions } from '@/modules/settings/settings-permissions'; const apiKeys: MenuLink = { id: 'api-keys', @@ -7,7 +8,14 @@ const apiKeys: MenuLink = { routeOptions: { query: { activeTab: 'api-keys' }, }, - display: () => true, + display: ({ user, tenant }) => { + const settingsPermissions = new SettingsPermissions( + tenant, + user, + ); + + return settingsPermissions.edit || settingsPermissions.lockedForCurrentPlan; + }, disable: () => false, }; diff --git a/frontend/src/modules/layout/config/links/plans-billing.ts b/frontend/src/modules/layout/config/links/plans-billing.ts index d90f6e890f..3b75ccaa91 100644 --- a/frontend/src/modules/layout/config/links/plans-billing.ts +++ b/frontend/src/modules/layout/config/links/plans-billing.ts @@ -1,4 +1,5 @@ import { MenuLink } from '@/modules/layout/types/MenuLink'; +import { SettingsPermissions } from '@/modules/settings/settings-permissions'; const plansBilling: MenuLink = { id: 'plans-billing', @@ -7,7 +8,14 @@ const plansBilling: MenuLink = { routeOptions: { query: { activeTab: 'plans' }, }, - display: () => true, + display: ({ user, tenant }) => { + const settingsPermissions = new SettingsPermissions( + tenant, + user, + ); + + return settingsPermissions.edit || settingsPermissions.lockedForCurrentPlan; + }, disable: () => false, }; diff --git a/frontend/src/modules/layout/config/links/users-permissions.ts b/frontend/src/modules/layout/config/links/users-permissions.ts index 0b58c17df5..7b11b13e58 100644 --- a/frontend/src/modules/layout/config/links/users-permissions.ts +++ b/frontend/src/modules/layout/config/links/users-permissions.ts @@ -1,4 +1,5 @@ import { MenuLink } from '@/modules/layout/types/MenuLink'; +import { SettingsPermissions } from '@/modules/settings/settings-permissions'; const usersPermissions: MenuLink = { id: 'users-permissions', @@ -7,7 +8,14 @@ const usersPermissions: MenuLink = { routeOptions: { query: { activeTab: 'users' }, }, - display: () => true, + display: ({ user, tenant }) => { + const settingsPermissions = new SettingsPermissions( + tenant, + user, + ); + + return settingsPermissions.edit || settingsPermissions.lockedForCurrentPlan; + }, disable: () => false, }; diff --git a/frontend/src/modules/report/pages/report-view-page-public.vue b/frontend/src/modules/report/pages/report-view-page-public.vue index e752378b5b..9cc961b825 100644 --- a/frontend/src/modules/report/pages/report-view-page-public.vue +++ b/frontend/src/modules/report/pages/report-view-page-public.vue @@ -267,7 +267,7 @@ export default { id: this.id, tenantId: this.tenantId, }); - this.currentTenant = await TenantService.find( + this.currentTenant = await TenantService.findName( this.tenantId, ); } else { diff --git a/frontend/src/modules/tenant/tenant-service.js b/frontend/src/modules/tenant/tenant-service.js index 23f1a9297e..4073832feb 100644 --- a/frontend/src/modules/tenant/tenant-service.js +++ b/frontend/src/modules/tenant/tenant-service.js @@ -119,6 +119,11 @@ export class TenantService { return response.data; } + static async findName(id) { + const response = await authAxios.get(`/tenant/${id}/name`); + return response.data; + } + static async findByUrl(url) { const response = await authAxios.get('/tenant/url', { params: { url }, diff --git a/frontend/src/modules/user/user-model.js b/frontend/src/modules/user/user-model.js index 5bf18ac4ca..ccc2e650e1 100644 --- a/frontend/src/modules/user/user-model.js +++ b/frontend/src/modules/user/user-model.js @@ -31,6 +31,7 @@ const fields = { }), password: new StringField('password', label('password'), { required: true, + matches: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/, }), passwordConfirmation: new StringField( 'passwordConfirmation',