mirror of
https://github.com/wassname/talk.git
synced 2026-07-04 05:36:46 +08:00
[CORL-476] Add badges to user from SSO token (#2470)
* fix: bug in lookup not checking object properly before accessing * fix: Recursive types not handling optional arrays * add user badges component * create user badges from sso token * update badges type * revert src/core/client/embed/index.html * remove duplicated line * add user badges component * create user badges from sso token * revert src/core/client/embed/index.html * remove duplicated line * fix types * fix tests and snaps * add user badges to user drawer * update snaps * update readme * [CORL-476] add user role from SSO token (#2475) * add role from token * use joi to validate role values Co-Authored-By: Wyatt Johnson <wyattjoh@gmail.com> * simplify sso.role validation * add test for invalid role in sso token
This commit is contained in:
@@ -196,6 +196,11 @@ You will then have to generate a JWT with the following claims:
|
||||
about status changes on a user account such as bans or suspensions.
|
||||
- `user.username` (**required**) - the username that should be used when being
|
||||
presented inside Coral to moderators and other users.
|
||||
- `user.badges` (_optional_) - array of strings to be displayed as badges beside
|
||||
username inside Coral, visible to other users and moderators. For example, to indicate
|
||||
a user's subscription status.
|
||||
- `user.role` (_optional_) - one of "COMMENTER", "STAFF", "MODERATOR", "ADMIN". Will create/update
|
||||
Coral user with this role.
|
||||
|
||||
An example of the claims for this token would be:
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { UserBadgesContainer_user as UserData } from "coral-admin/__generated__/UserBadgesContainer_user.graphql";
|
||||
import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { Tag } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
const UserBadgesContainer: FunctionComponent<Props> = ({ user }) => {
|
||||
if (!user.badges) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{user.badges.map(badge => (
|
||||
<Tag key={badge} color="dark" className={CLASSES.comment.userBadge}>
|
||||
{badge}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
user: graphql`
|
||||
fragment UserBadgesContainer_user on User {
|
||||
badges
|
||||
}
|
||||
`,
|
||||
})(UserBadgesContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -11,6 +11,7 @@ import { Button, Flex, Icon, Typography } from "coral-ui/components";
|
||||
|
||||
import RecentHistoryContainer from "./RecentHistoryContainer";
|
||||
import Tabs from "./Tabs";
|
||||
import UserBadgesContainer from "./UserBadgesContainer";
|
||||
import UserStatusDetailsContainer from "./UserStatusDetailsContainer";
|
||||
|
||||
import styles from "./UserHistoryDrawerContainer.css";
|
||||
@@ -38,8 +39,11 @@ const UserHistoryDrawerContainer: FunctionComponent<Props> = ({
|
||||
<Button className={styles.close} onClick={onClose}>
|
||||
<Icon size="md">close</Icon>
|
||||
</Button>
|
||||
<Flex className={styles.username}>
|
||||
<Flex className={styles.username} itemGutter>
|
||||
<span>{user.username}</span>
|
||||
<div>
|
||||
<UserBadgesContainer user={user} />
|
||||
</div>
|
||||
</Flex>
|
||||
<div className={styles.userStatus}>
|
||||
<Flex alignItems="center" itemGutter="half">
|
||||
@@ -130,6 +134,7 @@ const UserHistoryDrawerContainer: FunctionComponent<Props> = ({
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
user: graphql`
|
||||
fragment UserHistoryDrawerContainer_user on User {
|
||||
...UserBadgesContainer_user
|
||||
...UserStatusChangeContainer_user
|
||||
...UserStatusDetailsContainer_user
|
||||
...RecentHistoryContainer_user
|
||||
|
||||
@@ -95,6 +95,11 @@ const CLASSES = {
|
||||
*/
|
||||
userTag: "coral coral-comment-userTag",
|
||||
|
||||
/**
|
||||
* userBadge can be used to target a badge associated with a User.
|
||||
*/
|
||||
userBadge: "coral coral-comment-userBadge",
|
||||
|
||||
/**
|
||||
* commentTag can be used to target a tag associated with a Comment.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer";
|
||||
import { AuthorBadgesContainer_comment as CommentData } from "coral-stream/__generated__/AuthorBadgesContainer_comment.graphql";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { Tag } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
}
|
||||
|
||||
const AuthorBadgesContainer: FunctionComponent<Props> = ({ comment }) => {
|
||||
if (!comment.author || !comment.author.badges) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{comment.author.badges.map(badge => (
|
||||
<Tag key={badge} color="dark" className={CLASSES.comment.userBadge}>
|
||||
{badge}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment AuthorBadgesContainer_comment on Comment {
|
||||
author {
|
||||
badges
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(AuthorBadgesContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -27,6 +27,7 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
author: {
|
||||
id: "author-id",
|
||||
username: "Marvin",
|
||||
badges: [],
|
||||
},
|
||||
parent: null,
|
||||
body: "Woof",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { Button, Flex, HorizontalGutter, Tag } from "coral-ui/components";
|
||||
|
||||
import { isPublished } from "../helpers";
|
||||
import UserBadgesContainer from "./AuthorBadgesContainer";
|
||||
import ButtonsBar from "./ButtonsBar";
|
||||
import EditCommentFormContainer from "./EditCommentForm";
|
||||
import IndentedComment from "./IndentedComment";
|
||||
@@ -242,6 +243,7 @@ export class CommentContainer extends Component<Props, State> {
|
||||
user={comment.author}
|
||||
/>
|
||||
<UserTagsContainer comment={comment} />
|
||||
<UserBadgesContainer comment={comment} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -342,6 +344,7 @@ const enhanced = withSetCommentIDMutation(
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
badges
|
||||
role
|
||||
...UsernameWithPopoverContainer_viewer
|
||||
...ReactionButtonContainer_viewer
|
||||
@@ -389,6 +392,7 @@ const enhanced = withSetCommentIDMutation(
|
||||
...ReportButtonContainer_comment
|
||||
...CaretContainer_comment
|
||||
...RejectedTombstoneContainer_comment
|
||||
...AuthorBadgesContainer_comment
|
||||
...UserTagsContainer_comment
|
||||
}
|
||||
`,
|
||||
|
||||
+1
@@ -179,6 +179,7 @@ function commit(
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
createdAt: viewer.createdAt,
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
body: input.body,
|
||||
|
||||
+193
@@ -20,6 +20,7 @@ exports[`hide reply button 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -62,6 +63,7 @@ exports[`hide reply button 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -101,6 +103,7 @@ exports[`hide reply button 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
}
|
||||
@@ -111,6 +114,30 @@ exports[`hide reply button 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -156,6 +183,7 @@ exports[`renders body only 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": null,
|
||||
},
|
||||
@@ -204,6 +232,7 @@ exports[`renders body only 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": null,
|
||||
},
|
||||
@@ -243,6 +272,7 @@ exports[`renders body only 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": null,
|
||||
}
|
||||
@@ -253,6 +283,30 @@ exports[`renders body only 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": null,
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": null,
|
||||
},
|
||||
@@ -298,6 +352,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -346,6 +401,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -385,6 +441,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
}
|
||||
@@ -395,6 +452,30 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -440,6 +521,7 @@ exports[`renders disabled reply when story is closed 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -488,6 +570,7 @@ exports[`renders disabled reply when story is closed 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -527,6 +610,7 @@ exports[`renders disabled reply when story is closed 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
}
|
||||
@@ -537,6 +621,30 @@ exports[`renders disabled reply when story is closed 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -582,6 +690,7 @@ exports[`renders in reply to 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -634,6 +743,7 @@ exports[`renders in reply to 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -677,6 +787,7 @@ exports[`renders in reply to 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
}
|
||||
@@ -687,6 +798,34 @@ exports[`renders in reply to 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": Object {
|
||||
"author": Object {
|
||||
"username": "ParentAuthor",
|
||||
},
|
||||
},
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -736,6 +875,7 @@ exports[`renders username and body 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -784,6 +924,7 @@ exports[`renders username and body 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -823,6 +964,7 @@ exports[`renders username and body 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
}
|
||||
@@ -833,6 +975,30 @@ exports[`renders username and body 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -878,6 +1044,7 @@ exports[`shows conversation link 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -926,6 +1093,7 @@ exports[`shows conversation link 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
@@ -970,6 +1138,7 @@ exports[`shows conversation link 1`] = `
|
||||
<Relay(UsernameWithPopoverContainer)
|
||||
user={
|
||||
Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
}
|
||||
@@ -980,6 +1149,30 @@ exports[`shows conversation link 1`] = `
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
"body": "Woof",
|
||||
"createdAt": "1995-12-17T03:24:00.000Z",
|
||||
"editing": Object {
|
||||
"editableUntil": "1995-12-17T03:24:30.000Z",
|
||||
"edited": false,
|
||||
},
|
||||
"id": "comment-id",
|
||||
"lastViewerAction": null,
|
||||
"parent": null,
|
||||
"pending": false,
|
||||
"status": "NONE",
|
||||
"tags": Array [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(AuthorBadgesContainer)
|
||||
comment={
|
||||
Object {
|
||||
"author": Object {
|
||||
"badges": Array [],
|
||||
"id": "author-id",
|
||||
"username": "Marvin",
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ graphql`
|
||||
fragment CreateCommentMutation_viewer on User {
|
||||
role
|
||||
createdAt
|
||||
badges
|
||||
}
|
||||
`;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
@@ -149,6 +150,7 @@ function commit(
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
createdAt: viewer.createdAt,
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
revision: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
composes: tagText from "coral-ui/shared/typography.css";
|
||||
color: var(--palette-text-light);
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
padding: var(--spacing-1);
|
||||
white-space: nowrap;
|
||||
border-radius: 1px;
|
||||
display: inline-block;
|
||||
@@ -20,6 +20,10 @@
|
||||
background-color: var(--palette-error-dark);
|
||||
}
|
||||
|
||||
.colorDarkest {
|
||||
background-color: var(--palette-primary-darkest);
|
||||
}
|
||||
|
||||
.variantPill {
|
||||
border-radius: 20px;
|
||||
padding: 2px 6px;
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
*/
|
||||
classes: typeof styles;
|
||||
children: React.ReactNode;
|
||||
color?: "grey" | "primary" | "error";
|
||||
color?: "grey" | "primary" | "error" | "dark";
|
||||
|
||||
variant?: "regular" | "pill";
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const Tag: FunctionComponent<Props> = props => {
|
||||
[classes.colorPrimary]: color === "primary",
|
||||
[classes.colorError]: color === "error",
|
||||
[classes.colorGrey]: color === "grey",
|
||||
[classes.colorDarkest]: color === "dark",
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,14 @@ import { validate } from "coral-server/app/request/body";
|
||||
|
||||
describe("isSSOToken", () => {
|
||||
it("understands valid sso tokens", () => {
|
||||
const token = { user: { id: "id", email: "email", username: "username" } };
|
||||
const token = {
|
||||
user: {
|
||||
id: "id",
|
||||
email: "email",
|
||||
username: "username",
|
||||
role: "COMMENTER",
|
||||
},
|
||||
};
|
||||
expect(isSSOToken(token)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -20,6 +27,15 @@ describe("isSSOToken", () => {
|
||||
expect(
|
||||
isSSOToken({ user: { email: "email", username: "username" } } as object)
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
isSSOToken({
|
||||
user: {
|
||||
email: "email",
|
||||
username: "username",
|
||||
role: "SUPERADMIN",
|
||||
},
|
||||
} as object)
|
||||
).toBeFalsy();
|
||||
expect(isSSOToken({})).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface SSOUserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
badges?: string[];
|
||||
role?: GQLUSER_ROLE;
|
||||
}
|
||||
|
||||
export interface SSOToken {
|
||||
@@ -51,13 +53,17 @@ export function isSSOToken(token: SSOToken | object): token is SSOToken {
|
||||
return isNil(error);
|
||||
}
|
||||
|
||||
export const SSOUserProfileSchema = Joi.object().keys({
|
||||
id: Joi.string().required(),
|
||||
email: Joi.string()
|
||||
.lowercase()
|
||||
.required(),
|
||||
username: Joi.string().required(),
|
||||
});
|
||||
export const SSOUserProfileSchema = Joi.object()
|
||||
.keys({
|
||||
id: Joi.string().required(),
|
||||
email: Joi.string()
|
||||
.lowercase()
|
||||
.required(),
|
||||
username: Joi.string().required(),
|
||||
badges: Joi.array().items(Joi.string()),
|
||||
role: Joi.string().only(Object.values(GQLUSER_ROLE)),
|
||||
})
|
||||
.optionalKeys(["badges", "role"]);
|
||||
|
||||
export const SSOTokenSchema = Joi.object()
|
||||
.keys({
|
||||
@@ -88,7 +94,7 @@ export async function findOrCreateSSOUser(
|
||||
const {
|
||||
jti,
|
||||
exp,
|
||||
user: { id, email, username },
|
||||
user: { id, email, username, badges, role },
|
||||
iat,
|
||||
} = decodedToken;
|
||||
|
||||
@@ -127,7 +133,8 @@ export async function findOrCreateSSOUser(
|
||||
{
|
||||
id,
|
||||
username,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
role: role || GQLUSER_ROLE.COMMENTER,
|
||||
badges,
|
||||
email,
|
||||
profiles: [profile],
|
||||
},
|
||||
@@ -144,7 +151,7 @@ export async function findOrCreateSSOUser(
|
||||
mongo,
|
||||
tenant.id,
|
||||
user.id,
|
||||
{ email, username },
|
||||
{ email, username, badges, role: role || user.role },
|
||||
lastIssuedAt
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1525,6 +1525,10 @@ type User {
|
||||
"""
|
||||
avatar: String
|
||||
|
||||
"""
|
||||
badges are user display badges
|
||||
"""
|
||||
badges: [String!]
|
||||
"""
|
||||
email is the current email address for the User.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { SSOUserProfile } from "coral-server/app/middleware/passport/strategies/verifiers/sso";
|
||||
import { STAFF_ROLES } from "./constants";
|
||||
import { LocalProfile, SSOProfile, User } from "./user";
|
||||
|
||||
@@ -22,10 +25,15 @@ export function getSSOProfile(user: Pick<User, "profiles">) {
|
||||
}
|
||||
|
||||
export function needsSSOUpdate(
|
||||
token: Pick<User, "email" | "username">,
|
||||
user: Pick<User, "email" | "username">
|
||||
token: SSOUserProfile,
|
||||
user: Pick<User, "email" | "username" | "badges" | "role">
|
||||
) {
|
||||
return user.email !== token.email || user.username !== token.username;
|
||||
return (
|
||||
user.email !== token.email ||
|
||||
user.username !== token.username ||
|
||||
(token.role && user.role !== token.role) ||
|
||||
!isEqual(user.badges, token.badges)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -292,6 +292,12 @@ export interface User extends TenantResource {
|
||||
*/
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
*
|
||||
* badges are user display badges
|
||||
*/
|
||||
badges?: string[];
|
||||
|
||||
/**
|
||||
* emailVerificationID is used to store state regarding the verification state
|
||||
* of an email address to prevent replay attacks.
|
||||
@@ -668,6 +674,8 @@ export async function updateUserPassword(
|
||||
export interface UpdateUserInput {
|
||||
email?: string;
|
||||
username?: string;
|
||||
badges?: string[];
|
||||
role?: GQLUSER_ROLE;
|
||||
}
|
||||
|
||||
export async function updateUserFromSSO(
|
||||
@@ -677,7 +685,7 @@ export async function updateUserFromSSO(
|
||||
update: UpdateUserInput,
|
||||
lastIssuedAt: Date
|
||||
) {
|
||||
// Update the user with the new password.
|
||||
// Update the user with the new properties.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
tenantID,
|
||||
|
||||
Reference in New Issue
Block a user