Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
112 changes: 112 additions & 0 deletions apps/react/permissions/on-demand/custom/permissions-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# permissions-demo

## Overview

This demo showcases **custom** for **permissions** (on-demand) in **react**.

## Path

```
apps/react/permissions/on-demand/custom/permissions-demo/
```

## Package Name

`@apps/react-permissions-on-demand-custom-permissions-demo`

## Directory Structure

```
permissions-demo/
├── app/
│ ├── layout.tsx # Root layout
│ └── page.tsx # Main page
├── components/
│ ├── header/ # Header components (Velt notifications, etc.)
│ │ └── header.tsx
│ ├── sidebar/ # Sidebar components
│ │ └── sidebar.tsx
│ └── document/ # Main document/canvas logic
│ └── document-canvas.tsx
├── hooks/ # Custom React hooks
├── lib/ # Utility functions
│ └── utils.ts
├── public/ # Static assets
├── styles/ # Global styles
│ └── globals.css
├── .npmrc # pnpm config to prevent Tailwind v4 hoisting
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
├── components.json # shadcn/ui configuration
└── package.json
```

## Getting Started

### Install Dependencies

From the monorepo root:

```bash
pnpm install
```

### Run Development Server

```bash
cd apps/react/permissions/on-demand/custom/permissions-demo
pnpm dev
```

Or from the root:

```bash
pnpm --filter @apps/react-permissions-on-demand-custom-permissions-demo dev
```

### Build for Production

```bash
pnpm --filter @apps/react-permissions-on-demand-custom-permissions-demo build
```

## Structure

- **Framework**: react
- **Feature**: permissions
- **Document**: on-demand
- **Library**: custom
- **Demo**: permissions-demo

## Component Organization

- **`components/header/`** - Contains Velt components like notifications, presence indicators, header buttons
- **`components/sidebar/`** - Contains sidebar-related components
- **`components/document/`** - Contains the main application logic and custom integration
- **`hooks/`** - Custom React hooks for state management and side effects
- **`lib/`** - Utility functions and helpers

## Important Configuration

### .npmrc File
This demo includes a `.npmrc` file that prevents pnpm from hoisting Tailwind CSS v4 from other workspace packages. This is necessary because:
- This demo uses Tailwind CSS v3.4.x with traditional PostCSS configuration
- Other apps in the monorepo may use Tailwind CSS v4
- Without the `.npmrc`, pnpm would hoist v4 and cause PostCSS errors

**Do not delete the `.npmrc` file** - it ensures the correct Tailwind version is used.

## Next Steps

1. Add your custom implementation in `components/document/`
2. Add Velt collaboration features in `components/header/`
3. Update this README with specific usage instructions
4. Add the demo to `master-sample-app` if it should be showcased
5. Update deployment configs (Vercel, GitHub Actions) if needed

## Learn More

- [Monorepo Structure Guide](../../../../../README_MONOREPO.md)
- [Structure Documentation](../../../../../docs/structure.md)
- [Velt Documentation](https://docs.velt.dev)
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { NextRequest, NextResponse } from 'next/server';
import {
computeEffectiveAccess,
loadPermissionSettings,
loadSelectedUser,
PermissionSettings,
UserRole,
NodeType,
} from '@/lib/permissions-data';

// Velt Permission Query types
interface PermissionResource {
type: NodeType | 'context';
id: string;
source: string;
organizationId: string;
context?: Record<string, string | number>;
}

interface PermissionQueryRequest {
userId: string;
resource: PermissionResource;
}

interface PermissionQueryBody {
data: {
requests: PermissionQueryRequest[];
};
}

// Velt Permission Result types
interface PermissionResultItem {
userId: string;
resourceId: string;
type: string;
organizationId: string;
hasAccess: boolean;
accessRole?: 'viewer' | 'editor';
expiresAt?: number;
}

interface PermissionResultResponse {
data: PermissionResultItem[];
success: boolean;
statusCode: number;
message?: string;
}

// Map Velt resource IDs to our internal IDs
function mapResourceId(resourceId: string, resourceType: string): string {
// Our demo uses IDs like 'org-a', 'folder-a', 'doc-a'
// Velt might send different IDs, so we need to map them

// If it's already our format, return as-is
if (resourceId.startsWith('org-') || resourceId.startsWith('folder-') || resourceId.startsWith('doc-')) {
return resourceId;
}

// For the demo, we'll use the organization ID 'org-a' for organization type
if (resourceType === 'organization') {
return 'org-a';
}

return resourceId;
}

export async function POST(request: NextRequest): Promise<NextResponse<PermissionResultResponse>> {
try {
const body: PermissionQueryBody = await request.json();
const { requests } = body.data;

// Load current permission settings and user from the request headers or defaults
// In a real app, these would come from your database
// For this demo, we read from localStorage via cookies/headers or use defaults

let permissionSettings: PermissionSettings;
let selectedUser: UserRole;

// Try to get settings from custom headers (for demo purposes)
const settingsHeader = request.headers.get('x-demo-permission-settings');
const userHeader = request.headers.get('x-demo-selected-user');

if (settingsHeader) {
try {
permissionSettings = JSON.parse(settingsHeader);
} catch {
permissionSettings = loadPermissionSettings();
}
} else {
permissionSettings = loadPermissionSettings();
}

if (userHeader && ['Intern', 'Owner', 'Custom'].includes(userHeader)) {
selectedUser = userHeader as UserRole;
} else {
selectedUser = loadSelectedUser();
}
Copy link

Choose a reason for hiding this comment

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

Permission API ignores client-side settings changes

The permission API route calls loadPermissionSettings() and loadSelectedUser() as fallbacks, but these functions require localStorage which doesn't exist on the server. They will always return default values. The API expects settings via custom headers (x-demo-permission-settings, x-demo-selected-user), but no code in the codebase sends these headers. This means permission changes made in the UI are never reflected in actual permission checks - the demo UI appears to work but the underlying permission system always uses defaults.

Fix in Cursor Fix in Web


// Process each permission request
const permissions: PermissionResultItem[] = [];

for (const req of requests) {
const { userId, resource } = req;
const { type, id, organizationId } = resource;

// Handle context-based requests (return access based on org membership)
if (type === 'context') {
const orgAccess = computeEffectiveAccess('org-a', selectedUser, permissionSettings);
permissions.push({
userId,
resourceId: id,
type: 'context',
organizationId,
hasAccess: orgAccess.hasAccess,
});
continue;
}

// Map the resource ID to our internal ID
const internalId = mapResourceId(id, type);

// Compute access for this resource
const access = computeEffectiveAccess(internalId, selectedUser, permissionSettings);

const result: PermissionResultItem = {
userId,
resourceId: id,
type,
organizationId,
hasAccess: access.hasAccess,
};

// Add accessRole for documents
if (type === 'document' && access.hasAccess) {
result.accessRole = access.accessRole;
// Set expiration 10 minutes from now for demo
result.expiresAt = Date.now() + 10 * 60 * 1000;
}

permissions.push(result);
}

// Return response in Velt's expected format
const response: PermissionResultResponse = {
data: permissions,
success: true,
statusCode: 200,
message: 'Permissions validated successfully',
};

return NextResponse.json(response, { status: 200 });
} catch (error) {
console.error('Permission check error:', error);

return NextResponse.json(
{
data: [],
success: false,
statusCode: 500,
message: 'Internal server error',
},
{ status: 500 }
);
}
}

// Handle OPTIONS for CORS
export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-demo-permission-settings, x-demo-selected-user',
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server';

// [Velt] Replace with your own API key and auth token from https://console.velt.dev
const NEXT_PUBLIC_VELT_API_KEY = "YOUR_VELT_API_KEY";
const VELT_AUTH_TOKEN = "YOUR_VELT_AUTH_TOKEN";
Copy link

Choose a reason for hiding this comment

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

Bug: Placeholder API credentials prevent demo from working

The NEXT_PUBLIC_VELT_API_KEY and VELT_AUTH_TOKEN are set to placeholder strings ("YOUR_VELT_API_KEY" and "YOUR_VELT_AUTH_TOKEN"). Unlike other demos in this monorepo that have real API keys populated, this demo will fail to authenticate with the Velt API. The validation check on line 13 (if (!VELT_AUTH_TOKEN)) only catches empty/undefined values, not placeholder strings, so the request will proceed and fail at the external API call.

Fix in Cursor Fix in Web


export async function POST(req: NextRequest) {
try {
const { userId, organizationId, email, isAdmin } = await req.json();
if (!userId || !organizationId) {
return NextResponse.json({ error: 'Missing userId or organizationId' }, { status: 400 });
}
if (!VELT_AUTH_TOKEN) {
return NextResponse.json({ error: 'Server configuration error: missing VELT_AUTH_TOKEN' }, { status: 500 });
}
const body = {
data: {
userId,
userProperties: {
organizationId,
...(typeof isAdmin === 'boolean' ? { isAdmin } : {}),
...(email ? { email } : {}),
},
},
};
const res = await fetch('https://api.velt.dev/v2/auth/token/get', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-velt-api-key': NEXT_PUBLIC_VELT_API_KEY, 'x-velt-auth-token': VELT_AUTH_TOKEN },
body: JSON.stringify(body),
});
const json = await res.json();
const token = json?.result?.data?.token;
if (!res.ok || !token) {
return NextResponse.json({ error: json?.error?.message || 'Failed to generate token' }, { status: 500 });
}
return NextResponse.json({ token });
} catch {
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
Loading
Loading