Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Readonly User #637

Merged
merged 7 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions api/app/Http/Controllers/WorkspaceUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function addUser(Request $request, $workspaceId)

$this->validate($request, [
'email' => 'required|email',
'role' => 'required|in:admin,user',
'role' => 'required|in:' . implode(',', User::ROLES),
]);

$user = User::where('email', $request->email)->first();
Expand Down Expand Up @@ -62,10 +62,11 @@ private function inviteUser(Workspace $workspace, string $email, string $role)
{
if (
UserInvite::where('email', $email)
->where('workspace_id', $workspace->id)
->notExpired()
->pending()
->exists()) {
->where('workspace_id', $workspace->id)
->notExpired()
->pending()
->exists()
) {
return $this->success([
'message' => 'User has already been invited.'
]);
Expand All @@ -86,7 +87,7 @@ public function updateUserRole(Request $request, $workspaceId, $userId)
$this->authorize('adminAction', $workspace);

$this->validate($request, [
'role' => 'required|in:admin,user',
'role' => 'required|in:' . implode(',', User::ROLES),
]);

$workspace->users()->sync([
Expand Down
1 change: 1 addition & 0 deletions api/app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public function toArray($request)
'has_enterprise_subscription' => $this->has_enterprise_subscription,
'admin' => $this->admin,
'moderator' => $this->moderator,
'is_readonly' => $this->is_readonly,
'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms,
Expand Down
12 changes: 12 additions & 0 deletions api/app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class User extends Authenticatable implements JWTSubject

public const ROLE_ADMIN = 'admin';
public const ROLE_USER = 'user';
public const ROLE_READONLY = 'readonly';

public const ROLES = [
self::ROLE_ADMIN,
self::ROLE_USER,
self::ROLE_READONLY,
];

/**
* The attributes that are mass assignable.
Expand Down Expand Up @@ -118,6 +125,11 @@ public function getModeratorAttribute()
return in_array($this->email, config('opnform.moderator_emails')) || $this->admin;
}

public function getIsReadonlyAttribute()
{
return $this->workspaces()->where('role', self::ROLE_READONLY)->exists();
}

public function getTemplateEditorAttribute()
{
return $this->admin || in_array($this->email, config('opnform.template_editor_emails'));
Expand Down
18 changes: 13 additions & 5 deletions api/app/Policies/FormPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@ public function view(User $user, Form $form)
*/
public function create(User $user)
{
return true;
return !$user->is_readonly;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null check for consistency with TemplatePolicy

The create method should include a null check for consistency with TemplatePolicy and to prevent potential security issues.

-        return !$user->is_readonly;
+        return $user !== null && !$user->is_readonly;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return !$user->is_readonly;
return $user !== null && !$user->is_readonly;

}

/**
* Determine whether the user can perform write operations on the model.
*/
private function canPerformWriteOperation(User $user, Form $form): bool
{
return $user->ownsForm($form) && !$user->is_readonly;
}

/**
Expand All @@ -47,7 +55,7 @@ public function create(User $user)
*/
public function update(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}

/**
Expand All @@ -57,7 +65,7 @@ public function update(User $user, Form $form)
*/
public function delete(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}

/**
Expand All @@ -67,7 +75,7 @@ public function delete(User $user, Form $form)
*/
public function restore(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}

/**
Expand All @@ -77,6 +85,6 @@ public function restore(User $user, Form $form)
*/
public function forceDelete(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}
}
22 changes: 9 additions & 13 deletions api/app/Policies/TemplatePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,27 @@ class TemplatePolicy

/**
* Determine whether the user can create models.
*
* @return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
return $user !== null;
return $user !== null && !$user->is_readonly;
}

/**
* Determine whether the user can update the model.
*
* @return mixed
* Determine whether the user can perform write operations on the model.
*/
private function canPerformWriteOperation(User $user, Template $template): bool
{
return ($user->admin || $user->template_editor || $template->creator_id === $user->id) && !$user->is_readonly;
}

public function update(User $user, Template $template)
{
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
return $this->canPerformWriteOperation($user, $template);
}

/**
* Determine whether the user can delete the model.
*
* @return mixed
*/
public function delete(User $user, Template $template)
{
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
return $this->canPerformWriteOperation($user, $template);
}
}
6 changes: 5 additions & 1 deletion client/components/open/tables/OpenTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</p>
</resizable-th>
<th
v-if="hasActions"
class="n-table-cell p-0 relative"
style="width: 100px"
>
Expand Down Expand Up @@ -181,14 +182,14 @@ export default {
return {
workingFormStore,
form: storeToRefs(workingFormStore).content,
user: useAuthStore().user,
}
},

data() {
return {
tableHash: null,
skip: false,
hasActions: true,
internalColumns: [],
rafId: null,
fieldComponents: {
Expand All @@ -213,6 +214,9 @@ export default {
},

computed: {
hasActions() {
return !this.user.is_readonly
},
formData() {
return [...this.data].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
}
Expand Down
3 changes: 2 additions & 1 deletion client/components/pages/admin/AddUserToWorkspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const workspacesStore = useWorkspacesStore()

const roleOptions = [
{name: "User", value: "user"},
{name: "Admin", value: "admin"}
{name: "Admin", value: "admin"},
{name: "Read Only", value: "readonly"}
]

const newUser = ref("")
Expand Down
3 changes: 2 additions & 1 deletion client/components/pages/admin/EditWorkSpaceUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
:label="'New Role for '+props.user.name"
:options="[
{ name: 'User', value: 'user' },
{ name: 'Admin', value: 'admin' }
{ name: 'Admin', value: 'admin' },
{ name: 'Read Only', value: 'readonly' },
]"
option-key="value"
display-key="name"
Expand Down
3 changes: 3 additions & 0 deletions client/components/pages/auth/components/RegisterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
/>

<select-input
v-if="!disableEmail"
name="hear_about_us"
:options="hearAboutUsOptions"
:form="form"
Expand Down Expand Up @@ -155,6 +156,7 @@ export default {
form: useForm({
name: "",
email: "",
hear_about_us: "",
password: "",
password_confirmation: "",
agree_terms: false,
Expand Down Expand Up @@ -198,6 +200,7 @@ export default {
if (this.$route.query?.invite_token) {
if (this.$route.query?.email) {
this.form.email = this.$route.query?.email
this.form.hear_about_us = 'invite'
this.disableEmail = true
}
this.form.invite_token = this.$route.query?.invite_token
Expand Down
13 changes: 9 additions & 4 deletions client/components/pages/forms/show/ExtraMenu.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<template>
<div v-if="form">
<div v-if="form">
<div
v-if="loadingDuplicate || loadingDelete"
class="pr-4 pt-2"
>
<Loader class="h-6 w-6 mx-auto" />
</div>
<UDropdown v-else :items="items">
<UDropdown
v-else
:items="items"
>
<v-button
color="white"
>
Expand Down Expand Up @@ -128,8 +131,9 @@ const items = computed(() => {
}
}] : []
],
[
...props.isMainPage ? [{
...user.value.is_readonly ? [] : [
[
...props.isMainPage ? [{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
to: { name: 'forms-slug-edit', params: { slug: props.form.slug } }
Expand Down Expand Up @@ -166,6 +170,7 @@ const items = computed(() => {
class: 'text-red-800 hover:bg-red-50 hover:text-red-600 group',
iconClass: 'text-red-900 group-hover:text-red-800'
}
]
]
].filter((group) => group.length > 0)
})
Expand Down
16 changes: 11 additions & 5 deletions client/pages/forms/[slug]/show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</h2>
<div class="flex">
<extra-menu
v-if="!user.is_readonly"
class="mr-2"
:form="form"
/>
Expand Down Expand Up @@ -98,6 +99,7 @@
</svg>
</v-button>
<v-button
v-if="!user.is_readonly"
class="text-white"
:to="{ name: 'forms-slug-edit', params: { slug: slug } }"
>
Expand Down Expand Up @@ -244,6 +246,8 @@ useOpnSeoMeta({
title: "Home",
})

const authStore = useAuthStore()
const user = computed(() => authStore.user)
const route = useRoute()
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
Expand Down Expand Up @@ -279,11 +283,13 @@ const tabsList = [
route: "forms-slug-show-submissions",
params: { 'slug': slug }
},
{
name: "Integrations",
route: "forms-slug-show-integrations",
params: { 'slug': slug }
},
...user.value.is_readonly ? [] : [
{
name: "Integrations",
route: "forms-slug-show-integrations",
params: { 'slug': slug }
},
],
{
name: "Analytics",
route: "forms-slug-show-stats",
Expand Down
4 changes: 4 additions & 0 deletions client/pages/forms/[slug]/show/share.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<div class="mb-20">
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
<regenerate-form-link
v-if="!user.is_readonly"
class="sm:w-1/2 flex"
:form="props.form"
/>
Expand Down Expand Up @@ -54,6 +55,9 @@ import RegenerateFormLink from "~/components/pages/forms/show/RegenerateFormLink
import AdvancedFormUrlSettings from "~/components/open/forms/components/AdvancedFormUrlSettings.vue"
import EmbedFormAsPopupModal from "~/components/pages/forms/show/EmbedFormAsPopupModal.vue"

const authStore = useAuthStore()
const user = computed(() => authStore.user)

const props = defineProps({
form: { type: Object, required: true },
})
Expand Down
3 changes: 3 additions & 0 deletions client/pages/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Your Forms
</h2>
<v-button
v-if="!user.is_readonly"
v-track.create_form_click
:to="{ name: 'forms-create' }"
>
Expand Down Expand Up @@ -246,6 +247,8 @@ useOpnSeoMeta({
"All of your OpnForm are here. Create new forms, or update your existing forms.",
})

const authStore = useAuthStore()
const user = computed(() => authStore.user)
const subscriptionModalStore = useSubscriptionModalStore()
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
Expand Down
26 changes: 14 additions & 12 deletions client/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,20 @@ const tabsList = computed(() => {
name: "Profile",
route: "settings-profile",
},
{
name: "Workspace Settings",
route: "settings-workspace",
},
{
name: "Access Tokens",
route: "settings-access-tokens",
},
{
name: "Connections",
route: "settings-connections",
},
...user.value.is_readonly ? [] : [
{
name: "Workspace Settings",
route: "settings-workspace",
},
{
name: "Access Tokens",
route: "settings-access-tokens",
},
{
name: "Connections",
route: "settings-connections",
},
],
{
name: "Password",
route: "settings-password",
Expand Down
Loading