Skip to content

Commit 45ec7e8

Browse files
committed
authz & authn implement
1 parent 3050dc9 commit 45ec7e8

17 files changed

+149
-79
lines changed

schema/schema.graphql

+15-12
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@ directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
1919
"""Specifies required input field set from the base type for a resolver"""
2020
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
2121

22-
type AuthResponse {
23-
accessToken: String!
24-
expiresIn: Long!
25-
refreshExpiresIn: Long!
26-
refreshToken: String!
27-
scope: String!
28-
tokenType: String!
29-
}
30-
3122
type AuthResponseWithID {
3223
accessToken: String!
3324
expiresIn: Long!
@@ -43,18 +34,24 @@ enum Direction {
4334
DESC
4435
}
4536

37+
type InvalidEmailOrPasswordError {
38+
message: String!
39+
}
40+
4641
"""Local Date Time type"""
4742
scalar LocalDateTime
4843

44+
union LoginResponse = AuthResponseWithID | InvalidEmailOrPasswordError
45+
4946
"""Long type"""
5047
scalar Long
5148

5249
type Mutation {
53-
login(email: String!, password: String!): AuthResponseWithID!
50+
login(email: String!, password: String!): LoginResponse!
5451
postDelete(id: ID!): ID!
5552
postUpload(uploadedPost: UploadedPostInput!): UploadPost
56-
refresh(token: String!): AuthResponse!
57-
signUp(email: String!, name: String, password: String!): User!
53+
refresh(token: String!): AuthResponseWithID!
54+
signUp(email: String!, name: String, password: String!): SignUpResponse!
5855
}
5956

6057
interface Node {
@@ -104,6 +101,8 @@ type Query {
104101
users(before: ID, last: Int): UserConnect!
105102
}
106103

104+
union SignUpResponse = User | UserAlreadyExistError
105+
107106
type UploadPost {
108107
post: PostEdge!
109108
}
@@ -122,6 +121,10 @@ type User implements Node {
122121
roles: [String!]!
123122
}
124123

124+
type UserAlreadyExistError {
125+
message: String!
126+
}
127+
125128
type UserConnect {
126129
edges: [UserEdge!]!
127130
pageInfo: PageInfo!

src/components/Logout.tsx

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import { useRelayEnvironment } from "react-relay";
12
import { useAuth } from "../store/AuthContext";
23

34
export function Logout() {
5+
const enviroment = useRelayEnvironment();
6+
47
const auth = useAuth();
58

6-
return (
7-
<button
8-
onClick={auth.onLogout.bind(auth)}
9-
>
10-
Logout
11-
</button>
12-
);
9+
const handleLogout = () => {
10+
enviroment.commitUpdate((store) => {
11+
store.getRoot().getLinkedRecord("myInfo")?.invalidateRecord()
12+
});
13+
auth.onLogout();
14+
};
15+
16+
return <button onClick={handleLogout}>Logout</button>;
1317
}

src/components/Navbar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { useSnapshot } from "valtio";
22
import { Link } from "yarr";
3-
import { useAuth } from "../store/AuthContext";
3+
import { useAuth, useAuthSnapshot } from "../store/AuthContext";
44
import { Logout } from "./Logout";
55
import { UserInfo } from "./UserInfo";
66

77
export const Navbar = () => {
8-
const auth = useSnapshot(useAuth());
8+
const auth = useAuthSnapshot();
99

1010
return (
1111
<div className="sticky top-0 z-40 w-full">

src/components/Posts.tsx

+10-13
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ export const postsFragment = graphql`
2121
type: "[OrderInput!]"
2222
defaultValue: [{ property: "id", direction: DESC }]
2323
}
24-
search: { type: "String", defaultValue: null }
24+
search: { type: "String", defaultValue: "" }
2525
)
2626
@refetchable(queryName: "PostsQuery") {
2727
posts(first: $first, after: $after, orderBy: $orderBy, search: $search)
2828
@connection(
2929
key: "HomePostConnectionFragment_posts"
30-
filters: ["orderBy", "search"]
30+
filters: ["search"]
3131
) {
3232
__id
3333
edges {
@@ -52,17 +52,14 @@ export const Posts = ({ postsRef, search }: PostsProps) => {
5252

5353
const searching = useMemo(
5454
() =>
55-
debounce(
56-
(search: string) => {
57-
pagination.refetch({
58-
first: 10,
59-
after: null,
60-
orderBy: [{ property: "id", direction: "DESC" }],
61-
search: search,
62-
});
63-
},
64-
300,
65-
),
55+
debounce((search: string) => {
56+
pagination.refetch({
57+
first: 10,
58+
after: null,
59+
orderBy: [{ property: "id", direction: "DESC" }],
60+
search: search,
61+
});
62+
}, 300),
6663
[]
6764
);
6865

src/entry-client.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ const router = createBrowserRouter({
2323
});
2424

2525
const container = document.getElementById("app");
26-
/* ReactDOM.createRoot(container!!).render(
26+
ReactDOM.createRoot(container!!).render(
2727
<AppRoot router={router} relayEnvironment={relayEnvironment} auth={auth} />
28-
); */
29-
ReactDOM.hydrateRoot(
28+
);
29+
/* ReactDOM.hydrateRoot(
3030
container!!,
3131
<AppRoot router={router} relayEnvironment={relayEnvironment} auth={auth} />
32-
);
32+
); */
3333
/* ReactDOM.render(
3434
<AppRoot router={router} relayEnvironment={relayEnvironment} auth={auth} />,
3535
container

src/lib/request-context/ClientRequestContext.ts

+10
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@ import { MILLS_PER_SECOND, refresh, RequestContext } from "./RequestContext";
66
export class ClientRequestContext implements RequestContext {
77
isServer = false;
88

9+
userId?: string;
910
accessToken?: string;
1011
expiresIn?: number;
1112
refreshToken?: string;
1213
refreshExpiresIn?: number;
1314
refreshHandle?: NodeJS.Timeout;
1415

1516
constructor() {
17+
this.userId = Cookies.get("userId");
1618
this.accessToken = Cookies.get("accessToken");
1719
this.expiresIn = Number(Cookies.get("expiresIn"));
1820
this.refreshToken = Cookies.get("refreshToken");
1921
this.refreshExpiresIn = Number(Cookies.get("refreshExpiresIn"));
2022
const now = new Date().getTime();
2123

2224
if (now > this.expiresIn * MILLS_PER_SECOND) {
25+
this.userId = undefined
2326
this.accessToken = undefined;
2427
this.expiresIn = undefined;
28+
Cookies.remove("userId");
2529
Cookies.remove("accessToken");
2630
Cookies.remove("expiresIn");
2731
}
@@ -47,6 +51,7 @@ export class ClientRequestContext implements RequestContext {
4751
authInfo: AuthInfo,
4852
relayEnvironment: RelayModernEnvironment
4953
): void {
54+
setCookie("userId", authInfo.userId, authInfo.expiresIn);
5055
setCookie("accessToken", authInfo.accessToken, authInfo.expiresIn);
5156
setCookie("expiresIn", authInfo.expiresIn, authInfo.expiresIn);
5257
setCookie("refreshToken", authInfo.refreshToken, authInfo.refreshExpiresIn);
@@ -56,18 +61,21 @@ export class ClientRequestContext implements RequestContext {
5661
authInfo.refreshExpiresIn
5762
);
5863

64+
this.userId = authInfo.userId;
5965
this.accessToken = authInfo.accessToken;
6066
this.expiresIn = authInfo.expiresIn;
6167
this.refreshToken = authInfo.refreshToken;
6268
this.refreshExpiresIn = authInfo.refreshExpiresIn;
6369
}
6470

6571
onLogout(): void {
72+
this.userId = undefined;
6673
this.accessToken = undefined;
6774
this.expiresIn = undefined;
6875
this.refreshToken = undefined;
6976
this.refreshExpiresIn = undefined;
7077

78+
Cookies.remove("userId");
7179
Cookies.remove("accessToken");
7280
Cookies.remove("expiresIn");
7381
Cookies.remove("refreshToken");
@@ -93,6 +101,8 @@ function setCookie(name: string, value: string | number, expiresSec: number) {
93101
const v = typeof value === "string" ? value : value.toString();
94102
Cookies.set(name, v, {
95103
expires: new Date(expiresSec * MILLS_PER_SECOND),
104+
path: '/',
105+
// httpOnly: true,
96106
secure: true,
97107
sameSite: "Strict",
98108
});

src/lib/request-context/RequestContext.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { RequestContextRefreshMutation } from "./__generated__/RequestContextRef
55

66
export interface RequestContext {
77
isServer: boolean;
8+
9+
userId?: string;
810
accessToken?: string;
911
expiresIn?: number;
1012
refreshToken?: string;
@@ -37,6 +39,7 @@ export function refresh(
3739
mutation: graphql`
3840
mutation RequestContextRefreshMutation($token: String!) {
3941
refresh(token: $token) {
42+
userId
4043
accessToken
4144
expiresIn
4245
refreshToken

src/lib/request-context/ServerRequestContext.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { setCookie } from "h3";
22
import type { ServerResponse } from "http";
33
import RelayModernEnvironment from "relay-runtime/lib/store/RelayModernEnvironment";
44
import { AuthInfo } from "../../store/Auth";
5-
import { refresh, RequestContext } from "./RequestContext";
5+
import { MILLS_PER_SECOND, refresh, RequestContext } from "./RequestContext";
66
export class ServerRequestContext implements RequestContext {
77
isServer = true;
88

@@ -11,6 +11,11 @@ export class ServerRequestContext implements RequestContext {
1111
readonly res: ServerResponse
1212
) {}
1313

14+
get userId(): string | undefined {
15+
return this.cookies["userId"];
16+
}
17+
set userId(value: string | undefined) {}
18+
1419
get accessToken(): string | undefined {
1520
return this.cookies["accessToken"];
1621
}
@@ -37,6 +42,7 @@ export class ServerRequestContext implements RequestContext {
3742
authInfo: AuthInfo,
3843
relayEnvironment: RelayModernEnvironment
3944
): void {
45+
this.setCookie("userId", authInfo.userId, authInfo.expiresIn);
4046
this.setCookie("accessToken", authInfo.accessToken, authInfo.expiresIn);
4147
this.setCookie("expiresIn", authInfo.expiresIn, authInfo.expiresIn);
4248
this.setCookie(
@@ -59,7 +65,9 @@ export class ServerRequestContext implements RequestContext {
5965
setCookie(name: string, value: string | number, expireSec: number) {
6066
const v = typeof value === "string" ? value : value.toString();
6167
setCookie(this.res, name, v, {
62-
expires: new Date(1_000),
68+
expires: new Date(expireSec * MILLS_PER_SECOND),
69+
path: "/",
70+
// httpOnly: true,
6371
secure: true,
6472
sameSite: "strict",
6573
});

src/pages/Home.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import {
44
graphql,
55
PreloadedQuery,
66
useFragment,
7-
usePreloadedQuery,
7+
usePreloadedQuery
88
} from "react-relay";
9-
import { useSnapshot } from "valtio";
109
import { Link, RouteProps } from "yarr";
1110
import { Posts, postsFragment } from "../components/Posts";
1211
import { Progress } from "../components/Progress";
1312
import { PostsFragment_query$key } from "../components/__generated__/PostsFragment_query.graphql";
14-
import { useAuth } from "../store/AuthContext";
13+
import { useAuthSnapshot } from "../store/AuthContext";
1514
import { HomePostsQuery } from "./__generated__/HomePostsQuery.graphql";
1615

1716
export interface HomePageProps extends RouteProps<"/"> {
@@ -26,7 +25,7 @@ export const homePostsQuery = graphql`
2625
`;
2726

2827
export default function HomePage({ preloaded }: HomePageProps) {
29-
const auth = useSnapshot(useAuth());
28+
const auth = useAuthSnapshot();
3029

3130
const [search, setSearch] = useState("");
3231

src/pages/Login.tsx

+25-19
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ export default function LoginPage() {
2929
const [commit, isInFlight] = useMutation<LoginMutation>(graphql`
3030
mutation LoginMutation($email: String!, $password: String!) {
3131
login(email: $email, password: $password) {
32-
userId
33-
accessToken
34-
expiresIn
35-
refreshToken
36-
refreshExpiresIn
37-
scope
32+
__typename
33+
... on AuthResponseWithID {
34+
userId
35+
accessToken
36+
expiresIn
37+
refreshToken
38+
refreshExpiresIn
39+
scope
40+
}
41+
... on InvalidEmailOrPasswordError {
42+
message
43+
}
3844
}
3945
}
4046
`);
@@ -54,19 +60,19 @@ export default function LoginPage() {
5460
...data,
5561
},
5662
onCompleted: (response, errors) => {
57-
environment.commitUpdate((store) => {
58-
store.get(response.login.userId)?.invalidateRecord();
59-
});
60-
auth.onLoginSuccess(response.login, environment);
61-
navigation.push("/");
62-
},
63-
onError: (error) => {
64-
setError("password", {
65-
message: (error as any).source.errors
66-
.flatMap((e: any) => Object.values(e.extensions))
67-
.map((e: any) => e.message)
68-
.join(", "),
69-
});
63+
if (response.login.__typename === "AuthResponseWithID") {
64+
environment.commitUpdate((store) => {
65+
store.getRoot().getLinkedRecord("myInfo")?.invalidateRecord();
66+
});
67+
auth.onLoginSuccess(response.login, environment);
68+
navigation.push("/");
69+
} else if (
70+
response.login.__typename === "InvalidEmailOrPasswordError"
71+
) {
72+
setError("password", {
73+
message: response.login.message,
74+
});
75+
}
7076
},
7177
});
7278
};

0 commit comments

Comments
 (0)