Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: Add User layer. #2894

Draft
wants to merge 47 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c4977d5
refactor(core): docs + isolate client methods
victorgcramos Jan 26, 2023
5219ca2
feat(multi): add User Login
victorgcramos Jan 27, 2023
c6842aa
feat(core): add www policy slice
victorgcramos Jan 27, 2023
ed4527a
feat(multi): load www policy on signup page
victorgcramos Jan 30, 2023
2ebd8a8
feat(core): add pki extra store args
victorgcramos Jan 30, 2023
f6487dc
feat(core): add user signup
victorgcramos Jan 30, 2023
d97bd6f
feat(app/pi): use real signup on signup page
victorgcramos Jan 30, 2023
b12ec46
feat(multi): add User Email Verify
victorgcramos Jan 31, 2023
0db6120
feat(multi): add logout and persist user session
victorgcramos Jan 31, 2023
72e26ca
wip(multi): prepare user details
victorgcramos Jan 31, 2023
2c1a4db
test(app/core): fix recordsInventory unit tests
victorgcramos Feb 1, 2023
80d7e28
test(core): fix app setup tests
victorgcramos Feb 1, 2023
eb9964c
feat(core): add users slice
victorgcramos Feb 1, 2023
a4e8396
fix(core): fix users details fulfilled action
victorgcramos Feb 1, 2023
d98c893
feat(core): add createSubRouter app method
victorgcramos Feb 1, 2023
2bb1c8d
feat(app/pi): add sub-router on user details page
victorgcramos Feb 1, 2023
1f8b560
test(core): add appSetup createSubRouter tests
victorgcramos Feb 2, 2023
46fedae
test(core): fix usersSlice tests
victorgcramos Feb 2, 2023
4a1b396
fix(app/pi): user details tabs + author links
victorgcramos Feb 2, 2023
c28afd0
style(app/pi): user details page readability
victorgcramos Feb 2, 2023
06f8c81
feat(app/pi): improve user details account
victorgcramos Feb 2, 2023
d73c535
feat(app/pi): add credentials to User Identity
victorgcramos Feb 2, 2023
7c29573
feat(app/pi): get user preferences from users
victorgcramos Feb 2, 2023
6e80454
feat(core): add user manage thunk
victorgcramos Feb 2, 2023
0aff11f
feat(core): add User Edit thunk + tests
victorgcramos Feb 3, 2023
2a11826
feat(app/pi): save user details preferences
victorgcramos Feb 3, 2023
98441d8
feat(core): add appSetup route render condition
victorgcramos Feb 6, 2023
0a47835
test(core): appSetup route condition tests
victorgcramos Feb 6, 2023
dea1446
feat(core): add www api services
victorgcramos Feb 6, 2023
c1734a4
wip(app/pi): handle routes credentials
victorgcramos Feb 6, 2023
5a34177
test(app/pi): add tests for app routes credentials
victorgcramos Feb 8, 2023
6f6dbc0
feat(app/pi): display New Proposal button properly
victorgcramos Feb 13, 2023
bc2fda1
feat(app/pi): admin pages admin-only
victorgcramos Feb 13, 2023
3035835
feat(app/pi): add credentials to proposal new page
victorgcramos Feb 13, 2023
5ac229f
test(app/pi): add e2e tests for user credentials
victorgcramos Feb 13, 2023
836ee87
feat(core): add user records inventory
victorgcramos Feb 13, 2023
9a448b7
fix(core): user records inventory adjustments
victorgcramos Feb 14, 2023
c3aa11e
feat(app/pi): add user proposals
victorgcramos Feb 14, 2023
a1d6bb7
feat(core): add user payments
victorgcramos Feb 16, 2023
95c8ea4
feat(app/pi): get user credits from userPayments
victorgcramos Feb 16, 2023
ce9c43e
fix(core): app subrouter methods and flow
victorgcramos Feb 17, 2023
e51d5dd
fix(core): fix app views race condition
victorgcramos Feb 20, 2023
2429d09
feat(core): add user payments rescan
victorgcramos Feb 20, 2023
03d2312
fix(app/pi): admin subroutes fix
victorgcramos Feb 20, 2023
00f6455
fix(app/pi): use correct listeners for session
victorgcramos Feb 20, 2023
371af7d
feat(app/pi): improve user credits
victorgcramos Feb 20, 2023
0ef9cff
wip(core): add user identity module
victorgcramos Feb 20, 2023
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
229 changes: 229 additions & 0 deletions plugins-structure/apps/politeia/cypress/e2e/userCredentials.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import {
mockApi,
mockUser,
mockUserDetails,
} from "@politeiagui/core/dev/mocks";
import { mockTicketvoteInventory } from "@politeiagui/ticketvote/dev/mocks";

beforeEach(() => {
cy.mockResponse("/api/ticketvote/v1/inventory", mockTicketvoteInventory(0));
cy.viewport(1200, 1200);
});

describe("Given regular user is logged in", () => {
const currentid = "regular-user-id";
const otherid = "other-user-id";
beforeEach(() => {
cy.mockResponse("/api", mockApi({ activeusersession: true }), {
headers: {
"X-Csrf-Token": "csrf-test-token",
},
});
cy.mockResponse("/api/v1/user/me", mockUser({ userid: currentid }));
});
describe("When on user details page", () => {
it("should only display Identity, Account and Proposals on others profile", () => {
cy.mockResponse(
"/api/v1/user/" + otherid,
mockUserDetails({ id: otherid, email: "" })
);
cy.visit("/user/" + otherid);
cy.findByTestId("tabs-banner-tabs")
.should("contain.text", "Identity")
.and("contain.text", "Account")
.and("contain.text", "Submitted Proposals")
.and("not.contain.text", "Preferences")
.and("not.contain.text", "Credits")
.and("not.contain.text", "Draft Proposals")
.and("not.contain.text", "Two-Factor Authentication");
});
it("should display all tabs on current user profile", () => {
cy.mockResponse(
"/api/v1/user/" + currentid,
mockUserDetails({ id: currentid })
);
cy.visit("/user/" + currentid);
cy.findByTestId("tabs-banner-tabs")
.should("contain.text", "Identity")
.and("contain.text", "Account")
.and("contain.text", "Preferences")
.and("contain.text", "Credits")
.and("contain.text", "Submitted Proposals")
.and("contain.text", "Draft Proposals")
.and("contain.text", "Two-Factor Authentication");
});
it("should redirect to user details identity page on User details page", () => {
cy.mockResponse(
"/api/v1/user/" + otherid,
mockUserDetails({ id: otherid, email: "" })
);
cy.visit("/user/" + otherid);
const tabs = ["credits", "drafts", "2fa", "preferences"];
for (const tab of tabs) {
cy.visit("/user/" + otherid + "/" + tab);
cy.location("pathname").should("eq", "/user/" + otherid);
}
});
});

describe("when opening the header menu", () => {
it("should display the correct menu items", () => {
cy.mockResponse(
"/api/v1/user/" + currentid,
mockUserDetails({ id: currentid })
);
cy.visit("/user/" + currentid);
cy.findByTestId("header-dropdown").click();
cy.findByTestId("items-list").children().should("have.length", 5);
cy.findByTestId("header-dropdown").should("contain.text", "Account");
cy.findByTestId("header-dropdown").should(
"contain.text",
"All Proposals"
);
cy.findByTestId("header-dropdown").should("contain.text", "My Proposals");
cy.findByTestId("header-dropdown").should("contain.text", "My Drafts");
cy.findByTestId("header-dropdown").should("contain.text", "Logout");
});
});

describe("when navigating to home page", () => {
it("should display the New Proposal button", () => {
cy.mockResponse(
"/api/ticketvote/v1/inventory",
mockTicketvoteInventory(0)
);
cy.visit("/");
cy.findByTestId("banner-new-proposal-button").should("exist").click();
cy.findByTestId("record-form").should("be.visible");
});
});
});

describe("Given a non-logged in user", () => {
const userid = "other-user-id";
beforeEach(() => {
cy.mockResponse(
"/api/v1/user/" + userid,
mockUserDetails({ id: userid, email: "" })
);
});
// Same behavior for regular user's view and non-logged in user's view
describe("when on user details page", () => {
it("should only display Identity, Account and Proposals on others profile", () => {
cy.visit("/user/" + userid);
cy.findByTestId("tabs-banner-tabs")
.should("contain.text", "Identity")
.and("contain.text", "Account")
.and("contain.text", "Submitted Proposals")
.and("not.contain.text", "Preferences")
.and("not.contain.text", "Credits")
.and("not.contain.text", "Draft Proposals")
.and("not.contain.text", "Two-Factor Authentication");
});
});

describe("when opening the header menu", () => {
it("should not display dropdown button", () => {
cy.visit("/");
cy.findByTestId("header-dropdown").should("not.exist");
});
});

describe("when navigating to home page", () => {
it("should not display the New Proposal button", () => {
cy.visit("/");
cy.findByTestId("banner-new-proposal-button").should("not.exist");
cy.findByTestId("login-header-link").should("be.visible");
cy.findByTestId("signup-header-link").should("be.visible");
});
});

describe("when navigating to auth-required pages", () => {
it("should redirect to home page on Proposal New page", () => {
cy.visit("/record/new");
cy.location("pathname").should("eq", "/user/login");
});
it("should redirect to home page on Admin pages", () => {
cy.visit("/admin/records");
cy.location("pathname").should("eq", "/user/login");
});
it("should redirect to user details identity page on User details page", () => {
const tabs = ["credits", "drafts", "2fa", "preferences"];
for (const tab of tabs) {
cy.visit("/user/" + userid + "/" + tab);
cy.location("pathname").should("eq", "/user/" + userid);
}
});
});
});

describe("Given a logged in admin user", () => {
const currentid = "admin-user-id";
const otherid = "other-user-id";
beforeEach(() => {
cy.mockResponse("/api", mockApi({ activeusersession: true }), {
headers: {
"X-Csrf-Token": "csrf-test-token",
},
});
cy.mockResponse(
"/api/v1/user/me",
mockUser({ userid: currentid, isadmin: true })
);
});
describe("When on user details page", () => {
it("should display Identity, Account, Credits and Submitted Proposals tabs", () => {
cy.mockResponse(
"/api/v1/user/" + otherid,
mockUserDetails({ id: otherid })
);
cy.visit("/user/" + otherid);
cy.findByTestId("tabs-banner-tabs")
.should("contain.text", "Identity")
.and("contain.text", "Account")
.and("contain.text", "Credits")
.and("contain.text", "Submitted Proposals")
.and("not.contain.text", "Preferences")
.and("not.contain.text", "Draft Proposals")
.and("not.contain.text", "Two-Factor Authentication");
});

it("should display all tabs on current user profile", () => {
cy.mockResponse(
"/api/v1/user/" + currentid,
mockUserDetails({ id: currentid })
);
cy.visit("/user/" + currentid);
cy.findByTestId("tabs-banner-tabs")
.should("contain.text", "Identity")
.and("contain.text", "Account")
.and("contain.text", "Preferences")
.and("contain.text", "Credits")
.and("contain.text", "Submitted Proposals")
.and("contain.text", "Draft Proposals")
.and("contain.text", "Two-Factor Authentication");
});

it("should display the correct menu items", () => {
// Make screen bigger so the menu doesn't get hidden
cy.visit("/");
cy.findByTestId("header-dropdown").click();
cy.findByTestId("items-list").children().should("have.length", 7);
cy.findByTestId("header-dropdown").should("contain.text", "Account");
cy.findByTestId("header-dropdown").should(
"contain.text",
"All Proposals"
);
cy.findByTestId("header-dropdown").should("contain.text", "My Proposals");
cy.findByTestId("header-dropdown").should("contain.text", "My Drafts");
cy.findByTestId("header-dropdown").should("contain.text", "Logout");

// Admin items
cy.findByTestId("header-dropdown").should("contain.text", "Admin");
cy.findByTestId("header-dropdown").should(
"contain.text",
"Search for Users"
);
});
});
});
26 changes: 25 additions & 1 deletion plugins-structure/apps/politeia/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ import CommentsPlugin from "@politeiagui/comments";
import PiPlugin from "./pi";
// Global app services
import { downloadServicesListeners } from "./pi/downloads/listeners";
import { serviceListeners as authListeners } from "@politeiagui/core/user/auth/services";
import { serviceListeners as apiListeners } from "@politeiagui/core/api/services";
// Slices
import { userAuth } from "@politeiagui/core/user/auth";

/**
* This listener will be executed when the user has an active session on the
* server. The active session status is retrieved from api fetch fulfilled
* action.
*/
const loadActiveSession = authListeners.meOnLoad;

/**
* This listener will be executed when the user logs out. The logout action
* is dispatched by the userAuth slice. The listener will refresh the api
* client to clear the session cookie and renew the CSRF token.
*/
const refreshApiOnLogout = apiListeners.refreshApi.listenTo({
actionCreator: userAuth.logout.fulfilled,
});

const PoliteiaApp = appSetup({
plugins: [TicketvotePlugin, UiPlugin, CommentsPlugin, PiPlugin],
Expand All @@ -15,7 +35,11 @@ const PoliteiaApp = appSetup({
description:
"Politeia is the proposal system that is used to request funding from Decred's network treasury. The Decred stakeholders decide how treasury funds are allocated.",
},
setupServices: [...downloadServicesListeners],
setupServices: [
...downloadServicesListeners,
loadActiveSession,
refreshApiOnLogout,
],
});

export default PoliteiaApp;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Politeia will send you a link to verify your email address. You must open this
link in the same browser. After verifying your email, Politeia will create your
“identity”, which consists of a public/private cryptographic key pair and
browser cookie. This is necessary to verify your account and allow submission
of proposals, commenting, voting, and other Politeia functions. After
completing the signup process, you can export your identity keys to another
browser at any time.
Politeia will send you a link to verify your email address. **You must open this
link in the same browser**.

After verifying your email, Politeia will create your “identity”, which consists
of a public/private cryptographic key pair and browser cookie. This is necessary
to verify your account and allow submission of proposals, commenting, voting,
and other Politeia functions. After completing the signup process, you can
export your identity keys to another browser at any time.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { Button, H1 } from "pi-ui";
import styles from "./styles.module.css";
import { user } from "@politeiagui/core/user";
import { userAuth } from "@politeiagui/core/user/auth";

function BannerTitle({ title }) {
const currentUser = useSelector(user.selectCurrent);
const currentUser = useSelector(userAuth.selectCurrent);
return (
<div className={styles.bannerTitle}>
<H1>{title}</H1>
{currentUser && (
<a data-link href="/record/new">
<a
data-link
href="/record/new"
data-testid="banner-new-proposal-button"
>
<Button className={styles.fullScreenButton}>New Proposal</Button>
<Button size="sm" className={styles.xsScreenButton}>
+
Expand Down
36 changes: 26 additions & 10 deletions plugins-structure/apps/politeia/src/components/Header/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Navbar, ThemeToggle, theme } from "@politeiagui/common-ui/layout";
import LogoLight from "../../public/assets/images/pi-logo-light.svg";
import LogoDark from "../../public/assets/images/pi-logo-dark.svg";
import About from "../Static/About";
import { user } from "@politeiagui/core/user";
import styles from "./styles.module.css";
import { userAuth } from "@politeiagui/core/user/auth";

function PoliteiaLogo() {
const themeName = useSelector(theme.select);
Expand All @@ -31,13 +31,14 @@ function Item({ href, name, onClick }) {

function HeaderItems() {
const dispatch = useDispatch();
const currentUser = useSelector(user.selectCurrent);
const currentUser = useSelector(userAuth.selectCurrent);
function handleLogout() {
// TODO: Display logout modal
dispatch(user.logout());
dispatch(userAuth.logout());
}

return currentUser ? (
<div>
<div data-testid="header-dropdown">
<Dropdown
title={currentUser.username}
itemsListClassName={styles.headerItems}
Expand All @@ -49,28 +50,43 @@ function HeaderItems() {
name="My Proposals"
/>
<Item href={`/user/${currentUser.userid}/drafts`} name="My Drafts" />
{/* TODO: only show this for admin */}
<Item href="/admin/records" name="Admin" />
<Item href="/admin/search" name="Search for Users" />
{/* END TODO */}
{currentUser.isadmin && (
<>
<Item href="/admin/records" name="Admin" />
<Item href="/admin/search" name="Search for Users" />
</>
)}
<Item name="Logout" onClick={handleLogout} />
</Dropdown>
</div>
) : (
<>
<a href="/user/login" data-link>
<a href="/user/login" data-link data-testid="login-header-link">
Log in
</a>
<a href="/user/signup" data-link>
<a href="/user/signup" data-link data-testid="signup-header-link">
Sign up
</a>
</>
);
}

function ProposalCredits() {
const currentUser = useSelector(userAuth.selectCurrent);
if (!currentUser) return null;

const credits = currentUser.proposalcredits;
return (
<a href={`/user/${currentUser.userid}/credits`}>
{credits} Proposal Credits
</a>
);
}

function Header() {
return (
<Navbar logo={<PoliteiaLogo />} drawerContent={<About />}>
<ProposalCredits />
<ThemeToggle />
<HeaderItems />
</Navbar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ function ProposalSubtitle({
<div>
<RfpLinkedProposal name={rfpLink.name} link={rfpLink.link} />
<Join className={styles.proposalSubtitle}>
<Link data-link href={`user/${userid}`} data-testid="proposal-username">
<Link
data-link
href={`/user/${userid}`}
data-testid="proposal-username"
>
{username}
</Link>
{expireat && (
Expand Down
Loading