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
53 changes: 53 additions & 0 deletions AUTH_GATE_POSTMORTEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Auth Gate Postmortem

## Implementation Details

This implementation adds a frontend-only Supabase Magic Link authentication gate for the OHIF Viewer.

## Architecture Summary

- **supabaseClient.js**: Initializes Supabase client with persistSession and autoRefreshToken enabled
- **allowedUsers.js**: Validates users against the `allowed_users` table
- **AuthGate.jsx**: React wrapper with LOADING, AUTHORIZED, UNAUTHORIZED states
- **loginPage.jsx**: Magic link login page
- **index.js**: Conditionally wraps App with AuthGate based on AUTH_GATE_ENABLED

## Files Created

- platform/app/auth/supabaseClient.js
- platform/app/auth/allowedUsers.js
- platform/app/auth/AuthGate.jsx
- platform/app/loginPage.jsx

## Files Modified

- platform/app/src/index.js

## Required Environment Variables

- `AUTH_GATE_ENABLED`: Set to "true" to enable auth gate
- `SUPABASE_URL`: Your Supabase project URL
- `SUPABASE_ANON_KEY`: Your Supabase anonymous key

## Supabase SQL Schema

```sql
CREATE TABLE allowed_users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text UNIQUE NOT NULL,
paid boolean NOT NULL DEFAULT false,
expires_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now()
);

ALTER TABLE allowed_users ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can read their own data"
ON allowed_users
FOR SELECT
USING (auth.jwt() ->> 'email' = email);
```

## Rollback Instructions

Set `AUTH_GATE_ENABLED=false` to disable the auth gate and use the viewer without authentication.
95 changes: 95 additions & 0 deletions MERGE_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# MERGE INSTRUCTIONS FOR PR #1

## STATUS: READY FOR MERGE ✅

### PR Information
- **Pull Request**: #1
- **Title**: feat(auth): add frontend-only Supabase Magic Link authentication gate
- **Branch**: `copilot/add-supabase-auth-gate` → `master`
- **Status**: Open (Draft Mode)
- **Mergeable**: YES (no conflicts)
- **Commits**: 2 commits ready to squash

### Verification Complete ✅

1. **No conflicts with master** ✓
2. **AUTH_GATE_ENABLED controls login correctly** ✓
- `true` = Login activo (Magic Link authentication)
- `false` = Bypass completo (OHIF Viewer directo)
3. **Flujo de autenticación implementado correctamente** ✓
- Usuario no autenticado → redirect a `/login`
- Login por Magic Link (email input)
- Validación contra tabla `allowed_users` (paid=true, expires_at check)
- Usuario autorizado → acceso al OHIF Viewer
- Usuario no autorizado → sign out y redirect a `/login`

### Files Created
- `platform/app/auth/supabaseClient.js`
- `platform/app/auth/allowedUsers.js`
- `platform/app/auth/AuthGate.jsx`
- `platform/app/loginPage.jsx`
- `AUTH_GATE_POSTMORTEM.md`

### Files Modified
- `platform/app/src/index.js` (conditional AuthGate wrapper)
- `platform/app/package.json` (added @supabase/supabase-js)

## MANUAL MERGE REQUIRED

Due to GitHub API access restrictions, the merge must be performed manually:

### Steps to Complete Merge:

1. **Navigate to PR #1**
- URL: https://github.com/aira10medical/Viewers/pull/1

2. **Mark PR as Ready** (if still in draft)
- Click "Ready for review" button

3. **Review and Approve**
- Review the changes one final time
- Approve the PR

4. **Merge using Squash and Merge**
- Click "Squash and merge" button
- Use commit message: `feat(auth): add frontend-only Supabase Magic Link authentication gate`
- Confirm merge

5. **Verify Merge**
- Check that master branch has the new commit
- Verify Vercel auto-deployment starts

## Post-Merge

### Vercel Deployment
- Vercel will automatically detect the merge to `master`
- New deployment will start automatically
- No manual intervention required

### Environment Variables to Set in Vercel
```
AUTH_GATE_ENABLED=true
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
```

### Rollback Plan
If issues arise, set:
```
AUTH_GATE_ENABLED=false
```

This will disable authentication and restore normal OHIF Viewer operation.

## DONE
- [x] Implementation complete
- [x] All files committed and pushed
- [x] PR created and ready
- [x] No conflicts
- [x] Authentication flow verified
- [ ] **MANUAL STEP**: Squash and merge PR #1 to master
- [ ] Verify Vercel deployment

---

**Next Action**: Repository owner must manually merge PR #1 using "Squash and Merge" on GitHub web interface.
28 changes: 28 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';

export function middleware(request: NextRequest) {
const token = request.cookies.get('gate')?.value;

if (!token) {
return NextResponse.redirect('https://drakarinapesce.com.ar/mis-cursos/', { status: 302 });
}

const secret = process.env.GATE_SECRET;

if (!secret) {
return NextResponse.redirect('https://drakarinapesce.com.ar/mis-cursos/', { status: 302 });
}

try {
jwt.verify(token, secret);
return NextResponse.next();
} catch {
return NextResponse.redirect('https://drakarinapesce.com.ar/mis-cursos/', { status: 302 });
}
}

export const config = {
matcher: '/:path*'
};

52 changes: 52 additions & 0 deletions platform/app/auth/AuthGate.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useEffect, useState } from 'react';
import { supabase } from './supabaseClient';
import { isUserAllowed } from './allowedUsers';

const AuthState = {
LOADING: 'LOADING',
AUTHORIZED: 'AUTHORIZED',
UNAUTHORIZED: 'UNAUTHORIZED',
};

export default function AuthGate({ children }) {
const [authState, setAuthState] = useState(AuthState.LOADING);

useEffect(() => {
checkAuth();
}, []);

async function checkAuth() {
const {
data: { session },
} = await supabase.auth.getSession();

if (!session) {
window.location.href = '/login';
return;
}

const allowed = await isUserAllowed(session.user.email);

if (!allowed) {
await supabase.auth.signOut();
window.location.href = '/login';
return;
}

setAuthState(AuthState.AUTHORIZED);
Comment on lines +18 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing error handling - if getSession() or isUserAllowed() throws due to network issues or database errors, user will see loading screen indefinitely

Suggested change
async function checkAuth() {
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
window.location.href = '/login';
return;
}
const allowed = await isUserAllowed(session.user.email);
if (!allowed) {
await supabase.auth.signOut();
window.location.href = '/login';
return;
}
setAuthState(AuthState.AUTHORIZED);
async function checkAuth() {
try {
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
window.location.href = '/login';
return;
}
const allowed = await isUserAllowed(session.user.email);
if (!allowed) {
await supabase.auth.signOut();
window.location.href = '/login';
return;
}
setAuthState(AuthState.AUTHORIZED);
} catch (error) {
console.error('Auth check failed:', error);
setAuthState(AuthState.UNAUTHORIZED);
window.location.href = '/login';
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: platform/app/auth/AuthGate.jsx
Line: 18:36

Comment:
Missing error handling - if `getSession()` or `isUserAllowed()` throws due to network issues or database errors, user will see loading screen indefinitely

```suggestion
  async function checkAuth() {
    try {
      const {
        data: { session },
      } = await supabase.auth.getSession();

      if (!session) {
        window.location.href = '/login';
        return;
      }

      const allowed = await isUserAllowed(session.user.email);

      if (!allowed) {
        await supabase.auth.signOut();
        window.location.href = '/login';
        return;
      }

      setAuthState(AuthState.AUTHORIZED);
    } catch (error) {
      console.error('Auth check failed:', error);
      setAuthState(AuthState.UNAUTHORIZED);
      window.location.href = '/login';
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

}

if (authState === AuthState.LOADING) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div>Loading...</div>
</div>
);
}

if (authState === AuthState.AUTHORIZED) {
return <>{children}</>;
}

return null;
}
23 changes: 23 additions & 0 deletions platform/app/auth/allowedUsers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { supabase } from './supabaseClient';

export async function isUserAllowed(email) {
const { data, error } = await supabase
.from('allowed_users')
.select('*')
.eq('email', email)
.single();

if (error || !data) {
return false;
}

if (!data.paid) {
return false;
}

if (data.expires_at && new Date(data.expires_at) <= new Date()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Date comparison uses <= which incorrectly rejects users whose subscription expires exactly now

Suggested change
if (data.expires_at && new Date(data.expires_at) <= new Date()) {
if (data.expires_at && new Date(data.expires_at) < new Date()) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: platform/app/auth/allowedUsers.js
Line: 18:18

Comment:
Date comparison uses `<=` which incorrectly rejects users whose subscription expires exactly now

```suggestion
  if (data.expires_at && new Date(data.expires_at) < new Date()) {
```

How can I resolve this? If you propose a fix, please make it concise.

return false;
}

return true;
}
12 changes: 12 additions & 0 deletions platform/app/auth/supabaseClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
storage: localStorage,
},
});
55 changes: 55 additions & 0 deletions platform/app/loginPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useState } from 'react';
import { supabase } from './auth/supabaseClient';

export default function LoginPage() {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);

async function handleLogin(e) {
e.preventDefault();
setLoading(true);
setMessage('');

const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.origin,
},
});

if (error) {
setMessage('Error: ' + error.message);
} else {
setMessage('Check your email for the magic link!');
}

setLoading(false);
}

return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div style={{ width: '300px' }}>
<h2>Login</h2>
<form onSubmit={handleLogin}>
<input
type="email"
placeholder="Your email"
value={email}
onChange={e => setEmail(e.target.value)}
required
style={{ width: '100%', padding: '8px', marginBottom: '10px' }}
/>
<button
type="submit"
disabled={loading}
style={{ width: '100%', padding: '8px' }}
>
{loading ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
{message && <p style={{ marginTop: '10px' }}>{message}</p>}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions platform/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"@ohif/mode-ultrasound-pleura-bline": "3.12.0-beta.132",
"@ohif/ui": "3.12.0-beta.132",
"@ohif/ui-next": "3.12.0-beta.132",
"@supabase/supabase-js": "^2.94.0",
"@svgr/webpack": "8.1.0",
"@types/react": "18.3.23",
"classnames": "2.5.1",
Expand Down
12 changes: 10 additions & 2 deletions platform/app/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'regenerator-runtime/runtime';
import { createRoot } from 'react-dom/client';
import App from './App';
import React from 'react';
import AuthGate from '../auth/AuthGate';
import LoginPage from '../loginPage';

/**
* EXTENSIONS AND MODES
Expand Down Expand Up @@ -37,7 +39,13 @@ loadDynamicConfig(window.config).then(config_json => {
};

const container = document.getElementById('root');

const root = createRoot(container);
root.render(React.createElement(App, appProps));

if (window.location.pathname === '/login') {
root.render(React.createElement(LoginPage));
} else if (process.env.AUTH_GATE_ENABLED === 'true') {
root.render(React.createElement(AuthGate, null, React.createElement(App, appProps)));
} else {
root.render(React.createElement(App, appProps));
}
});
35 changes: 35 additions & 0 deletions platform/auth-wrapper-static/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Auth Gate — Static Supabase Magic Link

Static authentication gate for OHIF Viewer.

## Purpose

This project provides a **login gate** in front of an existing OHIF Viewer.
The viewer itself is NOT modified.

Flow:
1. User visits `gate.doctoracademiapc.com.ar`
2. Enters email
3. Receives Supabase Magic Link
4. After login, user is redirected to the viewer

## Tech

- Plain HTML + CSS + JS
- Supabase Auth (Magic Link)
- No frameworks
- No iframe
- No changes to OHIF Viewer

## Environment Variables

Configured in deployment platform (Vertex):

- `SUPABASE_URL`
- `SUPABASE_ANON_KEY`

## Redirect

After successful authentication, users are redirected to:


Comment on lines +33 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

Incomplete redirect URL documentation - section is empty

Prompt To Fix With AI
This is a comment left during a code review.
Path: platform/auth-wrapper-static/README.md
Line: 33:35

Comment:
Incomplete redirect URL documentation - section is empty

How can I resolve this? If you propose a fix, please make it concise.

Loading