Skip to content

Commit

Permalink
App: server side login + tweaks (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
zbycz authored Apr 18, 2024
1 parent 874e1ce commit b657139
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 52 deletions.
15 changes: 15 additions & 0 deletions pages/api/token-login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { serverFetchOsmUser } from '../../src/services/osmApiAuthServer';

// TODO upgrade Nextjs and use export async function POST(request: NextRequest) {
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const { osmAccessToken } = req.cookies;
const user = await serverFetchOsmUser({ osmAccessToken });

res.status(200).json({ user });
} catch (err) {
console.error(err); // eslint-disable-line no-console
res.status(err.code ?? 400).send(String(err));
}
};
4 changes: 2 additions & 2 deletions public/oauth-token.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
<h1>OsmAPP auth popup</h1>
<p>
[en] If this popup doesn't close automatically, you may close it and
proceed back to <a href="/">osmapp.org</a>.
proceed back to <a href="/">the app</a>.
</p>

<p>
[cs] Pokud se toto okno nezavře automaticky, můžete ho zavřít a
pokračovat na <a href="/">osmapp.org</a>.
pokračovat <a href="/">do aplikace</a>.
</p>

<img src="/osmapp/logo/osmapp.svg" />
Expand Down
10 changes: 5 additions & 5 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ const IndexWithProviders = () => {
);
};

const App = ({ featureFromRouter, initialMapView, hpCookie }) => {
const App = ({ featureFromRouter, initialMapView, cookies }) => {
const mapView = getMapViewFromHash() || initialMapView;
return (
<FeatureProvider featureFromRouter={featureFromRouter} hpCookie={hpCookie}>
<FeatureProvider featureFromRouter={featureFromRouter} cookies={cookies}>
<MapStateProvider initialMapView={mapView}>
<OsmAuthProvider>
<OsmAuthProvider cookies={cookies}>
<StarsProvider>
<EditDialogProvider /* TODO supply router.query */>
<IndexWithProviders />
Expand All @@ -113,10 +113,10 @@ const App = ({ featureFromRouter, initialMapView, hpCookie }) => {
App.getInitialProps = async (ctx) => {
await setIntlForSSR(ctx); // needed for lang urls like /es/node/123

const { hideHomepage: hpCookie } = nextCookies(ctx);
const cookies = nextCookies(ctx);
const featureFromRouter = await getInititalFeature(ctx);
const initialMapView = await getInitialMapView(ctx);
return { featureFromRouter, initialMapView, hpCookie };
return { featureFromRouter, initialMapView, cookies };
};

export default App;
3 changes: 1 addition & 2 deletions src/components/FeaturePanel/ObjectsAround.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import Router from 'next/router';
import { fetchAroundFeature } from '../../services/osmApi';
import { useFeatureContext } from '../utils/FeatureContext';
import { Feature } from '../../services/types';
import { getOsmappLink, getUrlOsmId } from '../../services/helpers';
import { FetchError, getOsmappLink, getUrlOsmId } from '../../services/helpers';
import Maki from '../utils/Maki';
import { t } from '../../services/intl';
import { FetchError } from '../../services/fetch';
import { trimText, useMobileMode } from '../helpers';
import { getLabel } from '../../helpers/featureLabel';
import { useUserThemeContext } from '../../helpers/theme';
Expand Down
21 changes: 12 additions & 9 deletions src/components/Map/TopMenu/HamburgerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,20 @@ const ThemeSelection = () => {

const UserLogin = forwardRef<HTMLLIElement, any>(({ closeMenu }, ref) => {
const { osmUser, handleLogin, handleLogout } = useOsmAuthContext();
const login = () => {
closeMenu();
handleLogin();
};
const logout = () => {
closeMenu();
setTimeout(() => {
handleLogout();
}, 100);
};

if (!osmUser) {
return (
<MenuItem ref={ref} onClick={handleLogin}>
<MenuItem ref={ref} onClick={login}>
<StyledAccountCircleIcon />
{t('user.login')}
</MenuItem>
Expand All @@ -178,18 +188,11 @@ const UserLogin = forwardRef<HTMLLIElement, any>(({ closeMenu }, ref) => {
<StyledAccountCircleIcon ref={ref} />
<strong>{osmUser}</strong>
</MenuItem>

<MenuItem onClick={handleLogout}>{t('user.logout')}</MenuItem>
<MenuItem onClick={logout}>{t('user.logout')}</MenuItem>
</>
);
});

// TODO maybe
// <ListItemIcon>
// <InboxIcon fontSize="small" />
// </ListItemIcon>
// <ListItemText primary="Inbox" />

// TODO custom Item components are not keyboard accesible
// seems like a bug in material-ui
// https://github.com/mui-org/material-ui/issues/22912
Expand Down
1 change: 1 addition & 0 deletions src/components/Map/TopMenu/LoginIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const StyledUserImg = styled.img`
width: 24px;
height: 24px;
border-radius: 50%;
background-color: white;
`;
export const LoginIcon = ({ onClick }) => {
const { osmUser, userImage } = useOsmAuthContext();
Expand Down
6 changes: 3 additions & 3 deletions src/components/utils/FeatureContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ export const FeatureContext = createContext<FeatureContextType>(undefined);
interface Props {
featureFromRouter: Feature | null;
children: ReactNode;
hpCookie: string;
cookies: Record<string, string>;
}

export const FeatureProvider = ({
children,
featureFromRouter,
hpCookie,
cookies,
}: Props) => {
const [preview, setPreview] = useState<Feature>(null);
const [feature, setFeature] = useState<Feature>(featureFromRouter);
Expand All @@ -49,7 +49,7 @@ export const FeatureProvider = ({
}, [featureFromRouter]);

const [homepageShown, showHomepage, hideHomepage] = useBoolState(
feature == null && hpCookie !== 'yes',
feature == null && cookies.hideHomepage !== 'yes',
);
const persistShowHomepage = () => {
setFeature(null);
Expand Down
14 changes: 9 additions & 5 deletions src/components/utils/OsmAuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { createContext, useContext, useState } from 'react';
import {
fetchOsmUser,
getOsmUser,
loginAndfetchOsmUser,
osmLogout,
OsmUser,
} from '../../services/osmApiAuth';
Expand All @@ -15,11 +14,16 @@ interface OsmAuthType {
handleLogout: () => void;
}

const useOsmUserState = (cookies) => {
const initialState = cookies.osmUserForSSR;
return useState<OsmUser | undefined>(initialState);
};

export const OsmAuthContext = createContext<OsmAuthType>(undefined);

export const OsmAuthProvider = ({ children }) => {
export const OsmAuthProvider = ({ children, cookies }) => {
const mapStateContext = useMapStateContext();
const [osmUser, setOsmUser] = useState<OsmUser | undefined>(getOsmUser());
const [osmUser, setOsmUser] = useOsmUserState(cookies);

const successfulLogin = (user: OsmUser) => {
setOsmUser(user);
Expand All @@ -29,7 +33,7 @@ export const OsmAuthProvider = ({ children }) => {
});
};

const handleLogin = () => fetchOsmUser().then(successfulLogin);
const handleLogin = () => loginAndfetchOsmUser().then(successfulLogin);
const handleLogout = () => osmLogout().then(() => setOsmUser(undefined));

const value = {
Expand Down
16 changes: 1 addition & 15 deletions src/services/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import fetch from 'isomorphic-unfetch';
import { getCache, getKey, writeCacheSafe } from './fetchCache';
import { isBrowser } from '../components/helpers';

export class FetchError extends Error {
constructor(
public message: string = '',
public code: string,
public data: string,
) {
super();
}

toString() {
const suffix = this.data && ` Data: ${this.data.substring(0, 1000)}`;
return `Fetch: ${this.message}${suffix}`;
}
}
import { FetchError } from './helpers';

// TODO cancel request in map.on('click', ...)
const abortableQueues: Record<string, AbortController> = {};
Expand Down
15 changes: 15 additions & 0 deletions src/services/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,18 @@ export const doShortenerRedirect = (ctx) => {

return false;
};

export class FetchError extends Error {
constructor(
public message: string = '',
public code: string,
public data: string,
) {
super();
}

toString() {
const suffix = this.data && ` Data: ${this.data.substring(0, 1000)}`;
return `Fetch: ${this.message}${suffix}`;
}
}
11 changes: 9 additions & 2 deletions src/services/osmApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { getApiId, getShortId, getUrlOsmId, OsmApiId, prod } from './helpers';
import { FetchError, fetchJson } from './fetch';
import {
FetchError,
getApiId,
getShortId,
getUrlOsmId,
OsmApiId,
prod,
} from './helpers';
import { fetchJson } from './fetch';
import { Feature, Position } from './types';
import { removeFetchCache } from './fetchCache';
import { overpassAroundToSkeletons } from './overpassAroundToSkeletons';
Expand Down
26 changes: 17 additions & 9 deletions src/services/osmApiAuth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Cookies from 'js-cookie';
import escape from 'lodash/escape';
import getConfig from 'next/config';
import { osmAuth } from 'osm-auth';
Expand Down Expand Up @@ -57,25 +58,32 @@ export const fetchOsmUser = async (): Promise<OsmUser> => {
path: '/api/0.6/user/details.json',
});
const details = JSON.parse(response).user;
const user = {
return {
name: details.display_name,
imageUrl:
details.img?.href ??
`https://www.gravatar.com/avatar/${details.id}?s=24&d=robohash`,
};

window.localStorage.setItem('osm_user', JSON.stringify(user));
return user;
};

export const getOsmUser = (): OsmUser | undefined =>
auth.authenticated()
? JSON.parse(window.localStorage.getItem('osm_user'))
: undefined;
export const loginAndfetchOsmUser = async (): Promise<OsmUser> => {
const osmUser = await fetchOsmUser();

const { url } = auth.options();
const osmAccessToken = localStorage.getItem(`${url}oauth2_access_token`);
const osmUserForSSR = JSON.stringify(osmUser);
Cookies.set('osmAccessToken', osmAccessToken, { path: '/', expires: 365 });
Cookies.set('osmUserForSSR', osmUserForSSR, { path: '/', expires: 365 });

await fetch('/api/token-login');

return osmUser;
};

export const osmLogout = async () => {
auth.logout();
window.localStorage.removeItem('osm_user');
Cookies.remove('osmAccessToken', { path: '/' });
Cookies.remove('osmUserForSSR', { path: '/' });
};

const getChangesetXml = ({ changesetComment, feature }) => {
Expand Down
47 changes: 47 additions & 0 deletions src/services/osmApiAuthServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fetch from 'isomorphic-unfetch';
import { FetchError } from './helpers';

interface OsmAuthFetchOpts extends RequestInit {
osmAccessToken: string;
}

const osmAuthFetch = async <T = any>(
endpoint: string,
options: OsmAuthFetchOpts,
): Promise<T> => {
const { osmAccessToken, ...restOptions } = options;
if (!osmAccessToken) throw new Error('No access token');

const url = `https://api.openstreetmap.org${endpoint}`;
const headers = {
'User-Agent': 'osmapp (SSR; https://osmapp.org/)',
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${osmAccessToken}`,
};

const response = await fetch(url, {
...restOptions,
headers,
});

if (!response.ok || response.status < 200 || response.status >= 300) {
const data = await response.text();
throw new FetchError(
`${response.status} ${response.statusText}`,
`${response.status}`,
data,
);
}

return response.json();
};

export const serverFetchOsmUser = async (
options: OsmAuthFetchOpts,
): Promise<{ id: number; username: string }> => {
const { user } = await osmAuthFetch('/api/0.6/user/details.json', options);
return {
id: user.id,
username: user.display_name,
};
};

0 comments on commit b657139

Please sign in to comment.