Skip to content

Commit ec8fa58

Browse files
committed
fix(auth): handle missing Supabase config
1 parent a61620d commit ec8fa58

8 files changed

Lines changed: 115 additions & 9 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VITE_SUPABASE_URL=
2+
VITE_SUPABASE_ANON_KEY=

src/App.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { SampleSettingsWindow } from "./components/SampleSettingsWindow";
1515
import { TopToolbar } from "./components/TopToolbar";
1616
import { setPlaying, undoLastChange, toggleWindowMaximize } from "./store";
1717
import { setUser, clearUser } from "./store/userSlice";
18-
import { supabase } from "./lib/supabase";
18+
import { isSupabaseConfigured, supabase } from "./lib/supabase";
1919
import { MIDI_FILE_DND_MIME } from "./utils/midiImport";
2020
import { MIDI_PATTERN_DND_MIME } from "./utils/midiPattern";
2121

@@ -288,6 +288,10 @@ function App() {
288288
}, []);
289289

290290
useEffect(function () {
291+
if (!isSupabaseConfigured) {
292+
return undefined;
293+
}
294+
291295
const syncUserProfile = async function (session) {
292296
try {
293297
const { data: profile } = await supabase

src/components/auth/AuthDialog.jsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { useState, useCallback } from "react";
22
import { useDispatch, useSelector } from "react-redux";
33
import { setUser, setAuthLoading, setAuthError } from "../../store/userSlice";
4-
import { supabase } from "../../lib/supabase";
4+
import {
5+
SUPABASE_UNCONFIGURED_MESSAGE,
6+
isSupabaseConfigured,
7+
supabase,
8+
} from "../../lib/supabase";
59

610
export function AuthDialog({ onClose }) {
711
const dispatch = useDispatch();
@@ -61,6 +65,11 @@ export function AuthDialog({ onClose }) {
6165

6266
const handleLogin = useCallback(
6367
async function () {
68+
if (!isSupabaseConfigured) {
69+
dispatch(setAuthError(SUPABASE_UNCONFIGURED_MESSAGE));
70+
return;
71+
}
72+
6473
dispatch(setAuthLoading(true));
6574
try {
6675
const { data: profile, error: profileError } = await supabase
@@ -113,6 +122,11 @@ export function AuthDialog({ onClose }) {
113122

114123
const handleRegister = useCallback(
115124
async function () {
125+
if (!isSupabaseConfigured) {
126+
dispatch(setAuthError(SUPABASE_UNCONFIGURED_MESSAGE));
127+
return;
128+
}
129+
116130
dispatch(setAuthLoading(true));
117131
try {
118132
const { data: existing } = await supabase

src/components/auth/AuthDialog.test.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { AuthDialog } from "./AuthDialog";
77
import { userReducer } from "../../store/userSlice";
88

99
vi.mock("../../lib/supabase", () => ({
10+
isSupabaseConfigured: false,
11+
SUPABASE_UNCONFIGURED_MESSAGE:
12+
"Cloud login is unavailable because Supabase is not configured.",
1013
supabase: {
1114
auth: { signInWithPassword: vi.fn(), signUp: vi.fn() },
1215
from: vi.fn().mockReturnValue({
@@ -58,6 +61,16 @@ describe("AuthDialog", () => {
5861
expect(screen.getByText(/Password must be at least 6 characters/i)).toBeInTheDocument();
5962
});
6063

64+
it("shows server configuration error when Supabase is unavailable", async () => {
65+
renderWithStore(<AuthDialog onClose={vi.fn()} />);
66+
await userEvent.type(screen.getByPlaceholderText("your_username"), "testuser");
67+
await userEvent.type(screen.getByPlaceholderText("••••••••"), "123456");
68+
69+
await userEvent.click(screen.getByRole("button", { name: /Sign In/i }));
70+
71+
expect(screen.getByText(/Supabase is not configured/i)).toBeInTheDocument();
72+
});
73+
6174
it("calls onClose when close button is clicked", async () => {
6275
const onClose = vi.fn();
6376
renderWithStore(<AuthDialog onClose={onClose} />);

src/components/auth/UserMenu.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ export function UserMenu({ onOpenAuth }) {
7777
type="button"
7878
className="project-dropdown-item"
7979
onClick={async function () {
80-
await supabase.auth.signOut();
80+
try {
81+
await supabase.auth.signOut();
82+
} catch (error) {
83+
console.warn("Could not sign out from Supabase", error);
84+
}
8185
dispatch(clearUser());
8286
setMenuOpen(false);
8387
}}

src/lib/projectApi.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { supabase } from "./supabase";
1+
import { assertSupabaseConfigured, supabase } from "./supabase";
22

33
function getProjectBpm(projectData) {
44
return projectData?.daw?.transport?.bpm || projectData?.transport?.bpm || 140;
@@ -9,6 +9,8 @@ function serializeProjectFile(projectData) {
99
}
1010

1111
async function getAuthenticatedUserId() {
12+
assertSupabaseConfigured();
13+
1214
const {
1315
data: { user },
1416
error,
@@ -99,6 +101,8 @@ export async function fetchProjects() {
99101
}
100102

101103
export async function loadProjectFromCloud(projectId) {
104+
assertSupabaseConfigured();
105+
102106
const { data: meta, error: metaError } = await supabase
103107
.from("projects")
104108
.select("storage_path")

src/lib/projectApi.test.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@ vi.mock("./supabase", () => {
66
from: vi.fn(),
77
storage: { from: vi.fn() },
88
};
9-
return { supabase: mockSupabase };
9+
return {
10+
SUPABASE_UNCONFIGURED_MESSAGE:
11+
"Cloud login is unavailable because Supabase is not configured.",
12+
assertSupabaseConfigured: vi.fn(),
13+
isSupabaseConfigured: true,
14+
supabase: mockSupabase,
15+
};
1016
});
1117

12-
import { supabase } from "./supabase";
18+
import {
19+
SUPABASE_UNCONFIGURED_MESSAGE,
20+
assertSupabaseConfigured,
21+
supabase,
22+
} from "./supabase";
1323
import {
1424
saveProjectToCloud,
1525
overwriteProjectInCloud,
@@ -24,9 +34,19 @@ describe("projectApi", () => {
2434

2535
beforeEach(() => {
2636
vi.clearAllMocks();
37+
assertSupabaseConfigured.mockImplementation(function () {});
2738
supabase.auth.getUser.mockResolvedValue({ data: { user: { id: userId } }, error: null });
2839
});
2940

41+
it("throws a clear error when Supabase is not configured", async () => {
42+
assertSupabaseConfigured.mockImplementation(function () {
43+
throw new Error(SUPABASE_UNCONFIGURED_MESSAGE);
44+
});
45+
46+
await expect(fetchProjects()).rejects.toThrow("Supabase is not configured");
47+
expect(supabase.auth.getUser).not.toHaveBeenCalled();
48+
});
49+
3050
describe("saveProjectToCloud", () => {
3151
it("uploads file and inserts metadata", async () => {
3252
const uploadMock = vi.fn().mockResolvedValue({ error: null });

src/lib/supabase.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,51 @@
11
import { createClient } from "@supabase/supabase-js";
22

3-
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
4-
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
3+
const supabaseUrl = String(import.meta.env.VITE_SUPABASE_URL || "").trim();
4+
const supabaseKey = String(import.meta.env.VITE_SUPABASE_ANON_KEY || "").trim();
55

6-
export const supabase = createClient(supabaseUrl, supabaseKey);
6+
export const SUPABASE_UNCONFIGURED_MESSAGE =
7+
"Cloud login is unavailable because Supabase is not configured.";
8+
9+
export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseKey);
10+
11+
export function assertSupabaseConfigured() {
12+
if (!isSupabaseConfigured) {
13+
throw new Error(SUPABASE_UNCONFIGURED_MESSAGE);
14+
}
15+
}
16+
17+
function createUnavailableSupabaseClient() {
18+
const rejectUnavailable = async function () {
19+
throw new Error(SUPABASE_UNCONFIGURED_MESSAGE);
20+
};
21+
22+
const throwUnavailable = function () {
23+
throw new Error(SUPABASE_UNCONFIGURED_MESSAGE);
24+
};
25+
26+
return {
27+
auth: {
28+
getUser: rejectUnavailable,
29+
signInWithPassword: rejectUnavailable,
30+
signUp: rejectUnavailable,
31+
signOut: rejectUnavailable,
32+
onAuthStateChange: function () {
33+
return {
34+
data: {
35+
subscription: {
36+
unsubscribe: function () {},
37+
},
38+
},
39+
};
40+
},
41+
},
42+
from: throwUnavailable,
43+
storage: {
44+
from: throwUnavailable,
45+
},
46+
};
47+
}
48+
49+
export const supabase = isSupabaseConfigured
50+
? createClient(supabaseUrl, supabaseKey)
51+
: createUnavailableSupabaseClient();

0 commit comments

Comments
 (0)