Skip to content

Commit 5e95496

Browse files
Merge branch 'main' into gsocmodule_C_week_3
2 parents 842d01f + 909fd43 commit 5e95496

13 files changed

Lines changed: 549 additions & 53 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ INSECURE_REQUESTS=false
3131

3232
CRE_ENABLE_HEALTH=false
3333

34+
# Expose MyOpenCRE in the UI (GET /api/capabilities → myopencre).
35+
# Off by default so production can roll out independently of import auth.
36+
37+
CRE_ENABLE_MYOPENCRE=false
38+
39+
# Expose Login/Logout in the UI (GET /api/capabilities → login).
40+
# Off by default until auth routes + User model land; enable on staging first.
41+
42+
CRE_ENABLE_LOGIN=false
43+
3444
# Embeddings
3545

3646
NO_GEN_EMBEDDINGS=false

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ install-python:
8585
install-typescript:
8686
yarn add webpack && cd application/frontend && yarn build
8787

88-
install: install-typescript install-python
88+
install: install-typescript install-python migrate-upgrade
8989

9090
docker-dev:
9191
docker build -f Dockerfile-dev -t opencre-dev:$(shell git rev-parse HEAD) .

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ Then edit `.env` and provide values appropriate for your environment.
289289
* Google Auth: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_SECRET_JSON`, `LOGIN_ALLOWED_DOMAINS`
290290
* GCP: `GCP_NATIVE`
291291
* Spreadsheet Auth: `OpenCRE_gspread_Auth`
292-
* Feature flags: `CRE_ENABLE_HEALTH` (enable the `GET /rest/v1/health` deploy/uptime probe; off by default, returns 404 when unset)
292+
* Feature flags: `CRE_ENABLE_HEALTH` (enable the `GET /rest/v1/health` deploy/uptime probe; off by default, returns 404 when unset), `CRE_ENABLE_MYOPENCRE` (expose MyOpenCRE in `GET /api/capabilities`; off by default), `CRE_ENABLE_LOGIN` (expose Login/Logout UI via `capabilities.login`; off by default)
293293

294294
See `.env.example` for full list and defaults.
295295

application/feature_flags.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ def is_cre_import_allowed() -> bool:
1616

1717
def is_health_endpoint_enabled() -> bool:
1818
return os.getenv("CRE_ENABLE_HEALTH", "").strip().lower() in TRUE_VALUES
19+
20+
21+
def is_myopencre_enabled() -> bool:
22+
"""Return True when the MyOpenCRE feature is enabled via CRE_ENABLE_MYOPENCRE."""
23+
return os.getenv("CRE_ENABLE_MYOPENCRE", "").strip().lower() in TRUE_VALUES
24+
25+
26+
def is_login_enabled() -> bool:
27+
"""Return True when auth UI / login is enabled via CRE_ENABLE_LOGIN."""
28+
return os.getenv("CRE_ENABLE_LOGIN", "").strip().lower() in TRUE_VALUES
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { useEnvironment } from './useEnvironment';
22
export { useLocationFromOutsideRoute } from './useLocationFromOutsideRoute';
33
export { useCapabilities } from './useCapabilities';
4+
export { useUser } from './useUser';

application/frontend/src/hooks/useCapabilities.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { useEnvironment } from './useEnvironment';
44

55
export type Capabilities = {
66
myopencre: boolean;
7+
login: boolean;
8+
};
9+
10+
const DEFAULT_CAPABILITIES: Capabilities = {
11+
myopencre: false,
12+
login: false,
713
};
814

915
export const useCapabilities = () => {
@@ -16,8 +22,13 @@ export const useCapabilities = () => {
1622

1723
fetch(`${baseUrl}/api/capabilities`)
1824
.then((res) => res.json())
19-
.then(setCapabilities)
20-
.catch(() => setCapabilities({ myopencre: false }))
25+
.then((data: Partial<Capabilities>) =>
26+
setCapabilities({
27+
myopencre: Boolean(data?.myopencre),
28+
login: Boolean(data?.login),
29+
})
30+
)
31+
.catch(() => setCapabilities(DEFAULT_CAPABILITIES))
2132
.finally(() => setLoading(false));
2233
}, [apiUrl]);
2334

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import { useEnvironment } from './useEnvironment';
4+
5+
export type UserState = {
6+
user: string | null;
7+
isLoggedIn: boolean;
8+
loading: boolean;
9+
};
10+
11+
export const useUser = () => {
12+
const { apiUrl } = useEnvironment();
13+
const [user, setUser] = useState<string | null>(null);
14+
const [loading, setLoading] = useState(true);
15+
16+
useEffect(() => {
17+
let active = true;
18+
fetch(`${apiUrl}/user`, { method: 'GET' })
19+
.then((res) => {
20+
if (res.status === 200) {
21+
return res.text();
22+
}
23+
if (res.status === 401) {
24+
return null; // the normal anonymous case — not logged in
25+
}
26+
// Unexpected status (e.g. 5xx): a real failure, not a clean anonymous state.
27+
throw new Error(`Unexpected /user status: ${res.status}`);
28+
})
29+
.then((value) => {
30+
if (active) {
31+
setUser(value && value.trim() !== '' ? value : null);
32+
}
33+
})
34+
.catch((err) => {
35+
// Network error or unexpected status. Degrade to anonymous so public
36+
// pages stay accessible (never block or redirect), but log the failure
37+
// instead of silently masking it as a normal logged-out state.
38+
if (active) {
39+
setUser(null);
40+
}
41+
console.error('useUser: could not resolve /user login state', err);
42+
})
43+
.finally(() => {
44+
if (active) {
45+
setLoading(false);
46+
}
47+
});
48+
return () => {
49+
active = false;
50+
};
51+
}, [apiUrl]);
52+
53+
const login = () => {
54+
window.location.href = `${apiUrl}/login`;
55+
};
56+
57+
const logout = () => {
58+
window.location.href = `${apiUrl}/logout`;
59+
};
60+
61+
return { user, isLoggedIn: user !== null, loading, login, logout };
62+
};

application/frontend/src/routes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface IRoute {
3737
}
3838
export interface Capabilities {
3939
myopencre: boolean;
40+
login: boolean;
4041
}
4142
export const ROUTES = (capabilities: Capabilities): IRoute[] => [
4243
...(capabilities.myopencre

application/frontend/src/scaffolding/Header/Header.tsx

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import './header.scss';
22

3-
import { Menu, Search } from 'lucide-react';
3+
import { LogOut, Menu, Search, User, X } from 'lucide-react';
44
import React, { useEffect, useState } from 'react';
55
import { Link, useHistory } from 'react-router-dom';
66
import { NavLink } from 'react-router-dom';
@@ -9,6 +9,7 @@ import { Button } from 'semantic-ui-react';
99
import { ClearFilterButton } from '../../components/FilterButton/FilterButton';
1010
import { Capabilities } from '../../hooks/useCapabilities';
1111
import { useLocationFromOutsideRoute } from '../../hooks/useLocationFromOutsideRoute';
12+
import { useUser } from '../../hooks/useUser';
1213
import { SearchBar } from '../../pages/Search/components/SearchBar';
1314
import { ROUTES } from '../../routes';
1415

@@ -26,6 +27,8 @@ export const Header = ({ capabilities }: HeaderProps) => {
2627
};
2728
const { showFilter } = useLocationFromOutsideRoute(routes);
2829

30+
const { user, isLoggedIn, loading: userLoading, login, logout } = useUser();
31+
2932
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
3033
useEffect(() => {
3134
const mediaQuery = window.matchMedia('(min-width: 768px)');
@@ -100,28 +103,24 @@ export const Header = ({ capabilities }: HeaderProps) => {
100103
</div>
101104

102105
<div className="navbar__actions">
103-
{/* Left these divs so that we can add auth functionality directly here. */}
104-
{/* <div className="navbar__desktop-auth">
105-
<Link
106-
to={{
107-
pathname: '/auth',
108-
state: { mode: 'login' },
109-
}}
110-
className="btn btn--ghost"
111-
>
112-
Log In
113-
</Link>
114-
115-
<Link
116-
to={{
117-
pathname: '/auth',
118-
state: { mode: 'signup' },
119-
}}
120-
className="btn btn--primary"
121-
>
122-
Sign Up
123-
</Link>
124-
</div> */}
106+
{capabilities.login && !userLoading && (
107+
<div className="navbar__desktop-auth">
108+
{isLoggedIn ? (
109+
<div className="user-info">
110+
<div className="user-details">
111+
<User className="icon" />
112+
<span>{user}</span>
113+
</div>
114+
<Button onClick={logout}>
115+
<LogOut className="icon" />
116+
Logout
117+
</Button>
118+
</div>
119+
) : (
120+
<Button onClick={login} content="Login" />
121+
)}
122+
</div>
123+
)}
125124

126125
<button className="navbar__mobile-menu-toggle" onClick={() => setIsMobileMenuOpen(true)}>
127126
<Menu className="icon" />
@@ -135,6 +134,13 @@ export const Header = ({ capabilities }: HeaderProps) => {
135134
<div className={`navbar__overlay ${isMobileMenuOpen ? 'is-open' : ''}`} onClick={closeMobileMenu}></div>
136135

137136
<div className={`navbar__mobile-menu ${isMobileMenuOpen ? 'is-open' : ''}`}>
137+
<button
138+
className="navbar__mobile-menu-close"
139+
onClick={closeMobileMenu}
140+
aria-label="Close mobile menu"
141+
>
142+
<X className="mobile-close-icon" />
143+
</button>
138144
<div className="mobile-search-container">
139145
<SearchBar />
140146
</div>
@@ -210,31 +216,37 @@ export const Header = ({ capabilities }: HeaderProps) => {
210216
)}
211217
</div>
212218

213-
<div className="mobile-auth">
214-
{/* <div className="auth-buttons">
215-
<Link
216-
to={{
217-
pathname: '/auth',
218-
state: { mode: 'login' },
219-
}}
220-
className="btn btn--ghost"
221-
onClick={closeMobileMenu}
222-
>
223-
Log In
224-
</Link>
225-
226-
<Link
227-
to={{
228-
pathname: '/auth',
229-
state: { mode: 'signup' },
230-
}}
231-
className="btn btn--primary"
232-
onClick={closeMobileMenu}
233-
>
234-
Sign Up
235-
</Link>
236-
</div> */}
237-
</div>
219+
{capabilities.login && !userLoading && (
220+
<div className="mobile-auth">
221+
{isLoggedIn ? (
222+
<div className="user-info">
223+
<div className="user-details">
224+
<User className="icon" />
225+
<span>{user}</span>
226+
</div>
227+
<Button
228+
onClick={() => {
229+
closeMobileMenu();
230+
logout();
231+
}}
232+
>
233+
<LogOut className="icon" />
234+
Logout
235+
</Button>
236+
</div>
237+
) : (
238+
<div className="auth-buttons">
239+
<Button
240+
onClick={() => {
241+
closeMobileMenu();
242+
login();
243+
}}
244+
content="Login"
245+
/>
246+
</div>
247+
)}
248+
</div>
249+
)}
238250
</div>
239251
</>
240252
);

application/frontend/src/scaffolding/Header/header.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,22 @@
345345
border: 0;
346346
}
347347

348+
.navbar__mobile-menu-close {
349+
display: flex;
350+
margin-left: auto;
351+
margin-bottom: 1rem;
352+
background: transparent;
353+
border: none;
354+
cursor: pointer;
355+
padding: 0.5rem;
356+
357+
.mobile-close-icon {
358+
color: #ffffff;
359+
width: 28px;
360+
height: 28px;
361+
}
362+
363+
&:hover {
364+
opacity: 0.7;
365+
}
366+
}

0 commit comments

Comments
 (0)