Skip to content

Commit 28c5240

Browse files
authored
User auth/management support (#27)
* User management implementation * optionally include auth related components * Use Ory kratos sdk instead of hardcoding URLs
1 parent 013d4de commit 28c5240

29 files changed

+1069
-103
lines changed

templates/.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
build

templates/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"@oryd/kratos-client": "^0.5.3-alpha.1",
67
"@testing-library/jest-dom": "^4.2.4",
78
"@testing-library/react": "^9.3.2",
89
"@testing-library/user-event": "^7.1.2",
910
"react": "^16.13.1",
1011
"react-dom": "^16.13.1",
12+
"react-router-dom": "^5.2.0",
1113
"react-scripts": "3.4.1"
1214
},
1315
"scripts": {
1416
"start": "react-scripts start",
1517
"build": "react-scripts build",
1618
"test": "react-scripts test",
17-
"eject": "react-scripts eject"
19+
"eject": "react-scripts eject",
20+
"lint": "eslint ."
1821
},
1922
"eslintConfig": {
2023
"extends": "react-app"
@@ -30,5 +33,8 @@
3033
"last 1 firefox version",
3134
"last 1 safari version"
3235
]
36+
},
37+
"devDependencies": {
38+
"eslint-plugin-react": "^7.21.5"
3339
}
3440
}

templates/src/App.css

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
1+
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');
2+
13
.App {
2-
text-align: center;
3-
background-color: white;
4-
font-family: 'Montserrat';
4+
font-family: 'Montserrat', sans-serif;
55
padding: 0;
66
margin: 0;
7+
height: 100%;
78
}
89

9-
.App-logo {
10-
width: calc(1/8 * 100vw)
11-
}
12-
13-
.App-header h1 {
14-
font-size: calc(1/12 * 100vw);
15-
margin: 0;
16-
margin-top: calc(-1/25 * 100vh);
17-
}
18-
19-
.App-header {
20-
min-height: 100vh;
10+
.content-container {
2111
display: flex;
12+
justify-content: space-around;
13+
margin-top: calc(1 / 20 * 100vh);
14+
width: 100%;
2215
flex-direction: column;
2316
align-items: center;
24-
justify-content: center;
25-
font-size: calc(10px + 2vmin);
26-
color: black;
2717
}

templates/src/App.js

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,86 @@
1-
import React, { useEffect, useState } from "react";
2-
import logo from "./commit-logo.png";
3-
import "./App.css";
1+
import React from 'react'
2+
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
43

5-
import "./components/Info";
6-
import InfoPanel from "./components/Info";
4+
import Home from './pages/Home'
5+
<%if eq (index .Params `userAuth`) "yes" %>
6+
import Auth from './pages/Auth'
7+
import Logout from './pages/Logout'<% end %>
78

8-
// Set config based on the environment variable the build was run under.
9-
let config = {};
10-
if (process.env.REACT_APP_CONFIG === "production") {
11-
config = require("./config/production.json");
12-
} else if (process.env.REACT_APP_CONFIG === "staging") {
13-
config = require("./config/staging.json");
14-
} else {
15-
config = require("./config/development.json");
16-
}
17-
18-
function App() {
19-
const [data, setData] = useState({
20-
info: {},
21-
error: null,
22-
});
23-
24-
const [status, setStatus] = useState({
25-
code: "Checking...",
26-
})
9+
import Dashboard from './pages/Dashboard'
10+
import PageNotFound from './pages/PageNotFound'
2711

28-
useEffect(() => {
29-
fetch(`${config.backendURL}/status/about`)
30-
.then(result => {
31-
setStatus({
32-
code: result.status,
33-
})
34-
return result.json()
35-
})
36-
.then(data => {
37-
setData({
38-
info: data,
39-
error: null
40-
})
41-
})
42-
.catch(error => {
43-
setData({
44-
info: {},
45-
error: error
46-
})
47-
});
48-
}, []);
12+
import './App.css'
13+
import Navigation from './components/Navigation'
14+
<%if eq (index .Params `userAuth`) "yes" %>
15+
import { AuthProvider } from './context/AuthContext'
4916

17+
function AuthRoutes () {
5018
return (
51-
<div className="App">
52-
<header className="App-header">
53-
<img src={logo} className="App-logo" alt="logo" />
54-
<h1>zero</h1>
55-
<InfoPanel data={data} status={status} config={config} />
56-
</header>
57-
</div>
58-
);
19+
<Switch>
20+
<Route path="/auth/login">
21+
<Auth page="login" title="Login" key="login" />
22+
</Route>
23+
<Route path="/auth/registration">
24+
<Auth page="registration" title="Regsiter" key="registration" />
25+
</Route>
26+
<Route path="/auth/profile">
27+
<Auth page="settings" title="profile" key="profile" />
28+
</Route>
29+
<Route path="/auth/recovery">
30+
<Auth page="recovery" title="Recovery" key="recovery" />
31+
</Route>
32+
<Route path="/auth/settings">
33+
<Auth page="settings" title="Settings" key="settings" />
34+
</Route>
35+
<Route path="/auth/logout">
36+
<Logout />
37+
</Route>
38+
</Switch>
39+
)
40+
}
41+
<% end %>
42+
function App() {
43+
return (
44+
<Router>
45+
<%if eq (index .Params `userAuth`) "yes" %>
46+
<AuthProvider>
47+
<div className="App">
48+
<Navigation />
49+
50+
<Switch>
51+
<Route exact path="/">
52+
<Home />
53+
</Route>
54+
<Route path="/dashboard">
55+
<Dashboard />
56+
</Route>
57+
<Route path="/auth/*">
58+
<AuthRoutes/>
59+
</Route>
60+
<Route path="*">
61+
<PageNotFound />
62+
</Route>
63+
</Switch>
64+
</div>
65+
</AuthProvider>
66+
<% else if eq (index .Params `userAuth`) "no" %>
67+
<div className="App">
68+
<Navigation />
69+
<Switch>
70+
<Route exact path="/">
71+
<Home />
72+
</Route>
73+
<Route path="/dashboard">
74+
<Dashboard />
75+
</Route>
76+
<Route path="*">
77+
<PageNotFound />
78+
</Route>
79+
</Switch>
80+
</div>
81+
<% end %>
82+
</Router>
83+
)
5984
}
6085

61-
export default App;
86+
export default App

templates/src/api/kratos.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import config from '../config';
2+
import { PublicApiAxiosParamCreator, AdminApiAxiosParamCreator } from '@oryd/kratos-client'
3+
4+
const capitalize = (str) => `${str[0].toUpperCase()}${str.slice(1)}`
5+
/**
6+
* Kratos redirects and sets cookie upon client lands on URL and redirects back to
7+
* frontend with request_id
8+
* */
9+
const authPublicURL = `${config.backendURL}/.ory/kratos/public`;
10+
const authAdminURL = `${config.backendURL}/.ory/kratos`;
11+
12+
/**
13+
* publicApi / adminApi are initialized Kratos SDK client with public / admin endpoints,
14+
* we use the sdk map out endpoints for http client to fetch
15+
*/
16+
const publicApi = new PublicApiAxiosParamCreator({basePath : authPublicURL });
17+
const adminApi = new AdminApiAxiosParamCreator({basePath : authAdminURL });
18+
19+
/**
20+
* getBrowserFlowParams / getRequestFlowData
21+
* maps and calls flows[Login/Registration/Recovery/Settings] to kratos sdk fn names
22+
*/
23+
const getBrowserFlowParams = async (flow) => publicApi[`initializeSelfService${flow}ViaBrowserFlow`]();
24+
const getRequestFlowData = async (flow, id) => publicApi[`getSelfService${flow}Flow`](id);
25+
26+
/**
27+
* generate<Logout/FormRequest/RequestData/Session>URL uses SDK to generate endpoint for http client
28+
*/
29+
const generateLogoutUrl = async () => {
30+
const { url } = await publicApi.initializeSelfServiceBrowserLogoutFlow();
31+
return authPublicURL + url;
32+
}
33+
34+
const generateFormRequestUrl = async (type) => {
35+
let { url } = await getBrowserFlowParams(capitalize(type));
36+
// Workaround for bug in SDK specs: https://github.com/ory/sdk/issues/43
37+
if (type == "settings") {
38+
url = url.replace(/\/flows$/, '');
39+
}
40+
return authPublicURL + url;
41+
}
42+
43+
const generateRequestDataUrl = async (type, flowId) => {
44+
const { url } = await getRequestFlowData(capitalize(type), flowId);
45+
return authAdminURL + url;
46+
};
47+
48+
const generateSessionUrl = async (type, flowId) => {
49+
const { url } = await publicApi.whoami();
50+
return authPublicURL + url;
51+
};
52+
53+
/**
54+
* fetchRequestData Assumes proxy(oathkeeper) to forward this request to admin-endpoint
55+
* and returns the form fields / destination
56+
* @param {*} type the form type (login/registration/recovery/settings)
57+
* @param {*} flowId the generated ID from the kratos redirect
58+
*/
59+
const fetchRequestData = async (type = "login", flowId) => {
60+
const uri = await generateRequestDataUrl(type, flowId);
61+
const options = { method: 'GET', headers: { Accept: 'application/json' } };
62+
const response = await fetch(uri, options);
63+
if (response.status >= 400 && response.status < 500) {
64+
return false;
65+
}
66+
return response.json();
67+
};
68+
69+
/**
70+
* fetchAuthState authenticates with the public endpoint of kratos,
71+
* given a valid session's cookie, kratos will respond with session information
72+
* that includes whether session is active, checking it then formatting for authContext
73+
*/
74+
const fetchAuthState = async() => {
75+
const uri = await generateSessionUrl();
76+
const options = {credentials: "include"};
77+
78+
const response = await fetch(uri, options);
79+
const authResult = await response.json();
80+
/** Kratos session payload example:
81+
* {
82+
"id": "872ea955-59c0-4417-add1-a9f824bb2f8d",
83+
"active": true,
84+
"expires_at": "2020-11-06T21:24:10.566549283Z",
85+
"authenticated_at": "2020-11-05T21:24:10.566549283Z",
86+
"issued_at": "2020-11-05T21:24:10.56655868Z",
87+
"identity": {
88+
"id": "db0eba56-f7e6-4a7c-8b8e-5ed3c802139c",
89+
"schema_id": "default",
90+
"schema_url": "https://<your-application>/.ory/kratos/public/schemas/default",
91+
"traits": {
92+
"email": "user@example.com"
93+
}
94+
}
95+
} */
96+
const isLoggedIn = !!authResult.active
97+
return {
98+
isAuthenticated: isLoggedIn,
99+
user: authResult.identity?.id,
100+
email: authResult.identity?.traits?.email
101+
};
102+
};
103+
104+
export { fetchRequestData, fetchAuthState, generateFormRequestUrl, generateLogoutUrl }

templates/src/commit-logo.png

-8 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
3+
function AuthCheck({authState, children}) {
4+
if (authState.isLoading) {
5+
return <div>authenticating...</div>
6+
}
7+
if (!authState.isAuthenticated) {
8+
return <div>Unauthenticated</div>
9+
}
10+
return <React.Fragment>{children}</React.Fragment>
11+
}
12+
13+
export default AuthCheck;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
.auth-form {
2+
min-width: 300px;
3+
}
4+
5+
.auth-form label {
6+
display: block;
7+
width: 100%;
8+
text-transform: capitalize;
9+
}
10+
11+
.auth-form a {
12+
color: inherit;
13+
display: block;
14+
text-align: right;
15+
text-decoration: none;
16+
font-size: 0.9em;
17+
}
18+
19+
.auth-form label input,
20+
.auth-form button {
21+
background: rgba(0, 0, 0, 0.2);
22+
border: 0px;
23+
box-sizing: border-box;
24+
font-size: 1.2em;
25+
margin-bottom: 1.2em;
26+
padding: 8px;
27+
width: 100%;
28+
}
29+
30+
.auth-form button {
31+
border: 3px transparent solid;
32+
cursor: pointer;
33+
font-weight: bold;
34+
font-size: 0.9em;
35+
}
36+
37+
.auth-form button.submit-button:hover {
38+
border: 3px solid rgba(0, 0, 0, 0.2);
39+
}
40+
41+
.field-error {
42+
color: red;
43+
margin: 0px 8px 12px;
44+
}
45+
46+
.oidc {
47+
color: white;
48+
text-transform: capitalize;
49+
}
50+
51+
.oidc:hover {
52+
opacity: 0.5;
53+
}
54+
55+
.oidc.google {
56+
background-color: #4B7EFF;
57+
}
58+
59+
.oidc.github {
60+
background-color: #562A7B;
61+
}
62+
63+
.oidc:before {
64+
content: 'Sign in with ';
65+
text-transform: none;
66+
}

0 commit comments

Comments
 (0)