Skip to content
This repository was archived by the owner on Dec 13, 2022. It is now read-only.

[Do not merge] User based access #3

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
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
137 changes: 116 additions & 21 deletions designer/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,135 @@ import ReactDOM from "react-dom";
import { LandingChoice, NewConfig, ChooseExisting } from "./pages/LandingPage";
import "./styles/index.scss";
import { initI18n } from "./i18n";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
useLocation,
} from "react-router-dom";
import Designer from "./designer";
import { SaveError } from "./pages/ErrorPages";
import { CookiesProvider, useCookies } from "react-cookie";
import { ProgressPlugin } from "webpack";

initI18n();

function NoMatch() {
return <div>404 Not found</div>;
}

function UserChoice() {
let [cookies, setcookie] = useCookies(["user"]);
let [userState, updateUserState] = React.useState(cookies.user);

let updateUser = (e: React.FormEvent<HTMLFormElement>) => {
setcookie("user", userState, {
path: "/",
sameSite: "strict",
});

return true;
};

// return (
// <form onSubmit={(e) => updateUser(e)}>
// <label htmlFor="user-picker">User select: </label>
// <input
// id="user-picker"
// name="user-picker"
// type="text"
// onChange={(e) => updateUserState(e.target.value)}
// value={userState}
// />
// <button type="submit">Submit</button>
// </form>
// );
return <div>Logged in as: {cookies.user}</div>;
}

function useQuery() {
const { search } = useLocation();

return React.useMemo(() => new URLSearchParams(search), [search]);
}

function AuthProvider({ children }) {
let [cookies, setCookie] = useCookies(["user"]);
let query = useQuery();

if (query.get("token")) {
setCookie("user", query.get("token"), {
path: "/",
sameSite: "strict",
});
}

if (cookies.user || query.get("token")) return children;

window.location.href = "/api/login";
}

function Auth() {
let [_, setcookie] = useCookies(["user"]);
let query = useQuery();

setcookie("user", query.get("token"), {
path: "/",
sameSite: "strict",
});
return <Redirect to="/" />;
}

function Logout() {
let [_, __, removeCookie] = useCookies(["user"]);

let doLogout = (e) => {
e.preventDefault();
removeCookie("user");
window.location.href = "/api/login";
};

return (
<a onClick={(e) => doLogout(e)} href="#">
Logout
</a>
);
}

export class App extends React.Component {
render() {
return (
<Router basename="/app">
<div id="app">
<Switch>
<Route path="/designer/:id" component={Designer} />
<Route path="/" exact>
<LandingChoice />
</Route>
<Route path="/new" exact>
<NewConfig />
</Route>
<Route path="/choose-existing" exact>
<ChooseExisting />
</Route>
<Route path="/save-error" exact>
<SaveError />
</Route>
<Route path="*">
<NoMatch />
</Route>
</Switch>
</div>
<CookiesProvider>
<div id="app">
<UserChoice />
<Logout />
<Switch>
<Route path="/auth" exact>
<Auth />
</Route>
<AuthProvider>
<Route path="/designer/:id" component={Designer} />
<Route path="/" exact>
<LandingChoice />
</Route>
<Route path="/new" exact>
<NewConfig />
</Route>
<Route path="/choose-existing" exact>
<ChooseExisting />
</Route>
<Route path="/save-error" exact>
<SaveError />
</Route>
<Route path="*">
<NoMatch />
</Route>
</AuthProvider>
</Switch>
</div>
</CookiesProvider>
</Router>
);
}
Expand Down
1 change: 1 addition & 0 deletions designer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"lodash": "^4.17.21",
"moment-timezone": "^0.5.31",
"nanoid": "^3.1.12",
"react-cookie": "^4.1.1",
"react-helmet": "^6.1.0",
"react-router-dom": "^5.2.0",
"resolve": "^1.19.0",
Expand Down
3 changes: 3 additions & 0 deletions designer/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Config {
previewUrl: string;
publishUrl: string;
formsApiUrl: string;
managementUrl: string;
persistentBackend: "s3" | "blob" | "preview" | "api";
s3Bucket?: string;
logLevel: "trace" | "info" | "debug" | "error";
Expand Down Expand Up @@ -39,6 +40,7 @@ const schema = joi.object({
previewUrl: joi.string(),
publishUrl: joi.string(),
formsApiUrl: joi.string(),
managementUrl: joi.string(),
persistentBackend: joi
.string()
.valid("s3", "blob", "preview", "api")
Expand All @@ -63,6 +65,7 @@ const config = {
previewUrl: process.env.PREVIEW_URL || "http://localhost:3009",
publishUrl: process.env.PUBLISH_URL || "http://localhost:3009",
formsApiUrl: process.env.FORMS_API_URL || "http://localhost:4567",
managementUrl: process.env.MANAGEMENT_URL || "http://localhost:3030",
persistentBackend: process.env.PERSISTENT_BACKEND || "preview",
s3Bucket: process.env.S3_BUCKET,
logLevel: process.env.LOG_LEVEL || "error",
Expand Down
60 changes: 56 additions & 4 deletions designer/server/lib/persistence/apiPersistenceService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PersistenceService } from "./persistenceService";
import Wreck from "@hapi/wreck";
import config from "../../config";
import { FormConfiguration } from "@xgovformbuilder/model";

export class ApiPersistenceService implements PersistenceService {
logger: any;
Expand All @@ -12,22 +13,73 @@ export class ApiPersistenceService implements PersistenceService {
});
}

async uploadConfigurationForUser(
id: string,
configuration: string,
user: string
): Promise<any> {
return Wreck.post(`${config.formsApiUrl}/publish`, {
payload: JSON.stringify({ id, configuration: JSON.parse(configuration) }),
headers: {
"x-api-key": user,
},
});
}

async copyConfiguration(configurationId: string, newName: string) {
const configuration = await this.getConfiguration(configurationId);
return this.uploadConfiguration(newName, configuration);
}

async listAllConfigurations() {
const { payload } = await Wreck.get(`${config.formsApiUrl}/published`);
return JSON.parse(payload.toString());
async copyConfigurationForUser(
configurationId: string,
newName: string,
user: string
): Promise<any> {
const configuration = await this.getConfigurationForUser(
configurationId,
user
);
return this.uploadConfigurationForUser(newName, configuration, user);
}

async getConfiguration(id: string) {
console.log("Getting: ", id);
const { payload } = await Wreck.get(
`${config.formsApiUrl}/published/${id}`
);
var configuration = JSON.parse(payload.toString()).values;
return JSON.stringify(configuration);
}

async getConfigurationForUser(id: string, user: string): Promise<string> {
const { payload } = await Wreck.get(
`${config.formsApiUrl}/published/${id}`,
{
headers: {
"x-api-key": user,
},
}
);

var configuration = JSON.parse(payload.toString()).values;
return JSON.stringify(configuration);
}

async listAllConfigurations() {
const { payload } = await Wreck.get(`${config.formsApiUrl}/published`);
return JSON.parse(payload.toString());
}

async listAllConfigurationsForUser(
user: string
): Promise<FormConfiguration[]> {
console.log("Getting forms for: ", user);

const { payload } = await Wreck.get(`${config.formsApiUrl}/published`, {
headers: {
"x-api-key": user,
},
});
return JSON.parse(payload.toString());
}
}
13 changes: 13 additions & 0 deletions designer/server/lib/persistence/persistenceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ export interface PersistenceService {
getConfiguration(id: string): Promise<string>;
uploadConfiguration(id: string, configuration: string): Promise<any>;
copyConfiguration(configurationId: string, newName: string): Promise<any>;
listAllConfigurationsForUser(user: string): Promise<FormConfiguration[]>;
getConfigurationForUser(id: string, user: string): Promise<string>;
uploadConfigurationForUser(
id: string,
configuration: string,
user: string
): Promise<any>;
copyConfigurationForUser(
configurationId: string,
newName: string,
user: string
): Promise<any>;
}

export class StubPersistenceService implements PersistenceService {
Expand All @@ -21,6 +33,7 @@ export class StubPersistenceService implements PersistenceService {
getConfiguration(_id: string) {
return Promise.resolve("");
}

copyConfiguration(_configurationId: string, _newName: string) {
return Promise.resolve("");
}
Expand Down
1 change: 1 addition & 0 deletions designer/server/plugins/designer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const designerPlugin = {
});

server.route(newConfig.registerNewFormWithRunner);
server.route(api.loginRedirect);
server.route(api.getFormWithId);
server.route(api.putFormWithId);
server.route(api.getAllPersistedConfigurations);
Expand Down
54 changes: 42 additions & 12 deletions designer/server/plugins/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@ const getPublished = async function (id) {
return payload.toString();
};

export const loginRedirect: ServerRoute = {
method: "GET",
path: "/api/login",
options: {
handler: (_, h) => {
return h.redirect(config.managementUrl);
},
},
};

export const getFormWithId: ServerRoute = {
// GET DATA
method: "GET",
path: "/api/{id}/data",
options: {
handler: async (request, h) => {
const { id } = request.params;
const { persistenceService } = request.services([]);
let formJson = newFormJson;
try {
const response = await getPublished(id);
const { values } = JSON.parse(response);

if (values) {
formJson = values;
}
const response = await persistenceService.getConfigurationForUser(
`${id}`,
request.state["user"]
);
formJson = JSON.parse(response);
} catch (error) {
request.logger.error(error);
}
Expand Down Expand Up @@ -61,11 +71,22 @@ export const putFormWithId: ServerRoute = {

throw new Error("Schema validation failed, reason: " + error.message);
}
await persistenceService.uploadConfiguration(
`${id}`,
JSON.stringify(value)
);
await publish(id, value);

if (request.state["user"]) {
await persistenceService.uploadConfigurationForUser(
`${id}`,
JSON.stringify(value),
request.state["user"]
);
} else {
await persistenceService.uploadConfiguration(
`${id}`,
JSON.stringify(value)
);
}

// Remove publishing for now
// await publish(id, value);
return h.response({ ok: true }).code(204);
} catch (err) {
request.logger.error("Designer Server PUT /api/{id}/data error:", err);
Expand All @@ -89,7 +110,16 @@ export const getAllPersistedConfigurations: ServerRoute = {
handler: async (request, h): Promise<ResponseObject | undefined> => {
const { persistenceService } = request.services([]);
try {
const response = await persistenceService.listAllConfigurations();
let response;

if (request.state["user"]) {
response = await persistenceService.listAllConfigurationsForUser(
request.state["user"]
);
} else {
response = await persistenceService.listAllConfigurations();
}

return h.response(response).type("application/json");
} catch (error) {
request.server.log(["error", "/configurations"], error);
Expand Down
Loading