Skip to content

Commit aec1f4a

Browse files
committedMay 6, 2019
auth has been implemented
1 parent c4e51e1 commit aec1f4a

File tree

10 files changed

+337
-4
lines changed

10 files changed

+337
-4
lines changed
 

‎packages/server/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"lint": "echo linting disabled"
1212
},
1313
"dependencies": {
14+
"isomorphic-fetch": "^2.2.1",
15+
"jsonwebtoken": "^8.5.1",
1416
"yoga": "0.0.18"
1517
},
1618
"devDependencies": {
@@ -27,4 +29,4 @@
2729
},
2830
"license": "MIT",
2931
"version": "1.0.1"
30-
}
32+
}

‎packages/server/prisma/datamodel.prisma

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
type User {
22
id: ID! @unique
3-
email: String! @unique
3+
createdAt: DateTime!
4+
updatedAt: DateTime!
5+
46
name: String!
7+
email: String @unique
8+
9+
githubHandle: String! @unique
10+
githubUserId: String! @unique
11+
avatarUrl: String
12+
bio: String!
13+
514
posts: [Post!]!
615
}
716

‎packages/server/src/config.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type Config = {
2+
github: {
3+
CLIENT_ID: string;
4+
CLIENT_SECRET: string;
5+
},
6+
jwt: {
7+
SECRET: string
8+
}
9+
};
10+
// https://github.com/login/oauth/authorize?client_id=Iv1.5345771c55b8eb37&redirect_uri=http://localhost:8000
11+
const config: Config = {
12+
github: {
13+
CLIENT_ID: process.env.GQL_GITHUB_CLIENT_ID || "Iv1.5345771c55b8eb37",
14+
CLIENT_SECRET: process.env.GQL_GITHUB_CLIENT_SECRET || "19cef65a19b44c2c28e86f3969d83583cf071b02"
15+
},
16+
jwt: {
17+
SECRET: process.env.GQL_JWT_SECRET || "helloworld"
18+
}
19+
}
20+
21+
export default config;

‎packages/server/src/context.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { yogaContext } from 'yoga'
33

44
export interface Context {
55
prisma: Prisma
6+
req: Request
67
}
78

89
export default yogaContext(({ req }) => ({

‎packages/server/src/github.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as fetch from 'isomorphic-fetch'
2+
import config from './config';
3+
4+
export interface GithubUser {
5+
login: string;
6+
id: number;
7+
node_id: string;
8+
avatar_url: string;
9+
gravatar_id: string;
10+
url: string;
11+
html_url: string;
12+
followers_url: string;
13+
following_url: string;
14+
gists_url: string;
15+
starred_url: string;
16+
subscriptions_url: string;
17+
organizations_url: string;
18+
repos_url: string;
19+
events_url: string;
20+
received_events_url: string;
21+
type: string;
22+
site_admin: boolean;
23+
name: string;
24+
company: string;
25+
blog: string;
26+
location: string;
27+
email: string;
28+
hireable?: any;
29+
bio: string;
30+
public_repos: number;
31+
public_gists: number;
32+
followers: number;
33+
following: number;
34+
created_at: string;
35+
updated_at: string;
36+
}
37+
38+
export async function getGithubToken(githubCode: string): Promise<string> {
39+
const endpoint = 'https://github.com/login/oauth/access_token'
40+
const data = await fetch(endpoint, {
41+
method: 'POST',
42+
headers: {
43+
'Content-Type': 'application/json',
44+
'Accept': 'application/json'
45+
},
46+
body: JSON.stringify({
47+
client_id: config.github.CLIENT_ID,
48+
client_secret: config.github.CLIENT_SECRET,
49+
code: githubCode,
50+
})
51+
})
52+
.then(response => response.json())
53+
54+
if (data.error) {
55+
throw new Error(JSON.stringify(data.error))
56+
}
57+
58+
return data.access_token
59+
}
60+
61+
62+
export async function getGithubUser(githubToken: string): Promise<GithubUser> {
63+
const endpoint = `https://api.github.com/user?access_token=${githubToken}`
64+
const data = await fetch(endpoint)
65+
.then(response => response.json())
66+
67+
if (data.error) {
68+
throw new Error(JSON.stringify(data.error))
69+
}
70+
71+
return data
72+
}

‎packages/server/src/graphql/Viewer.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { queryField, objectType } from "yoga";
2+
import { getUserId } from "../utils";
3+
4+
export const Viewer = objectType({
5+
name: "Viewer",
6+
definition: (t) => {
7+
t.field("user", {
8+
type: "User",
9+
resolve: async (_, args, ctx) => {
10+
const id = getUserId(ctx)
11+
return await ctx.prisma.user({ id })
12+
}
13+
})
14+
}
15+
});
16+
17+
export const viewer = queryField("viewer", {
18+
type: Viewer,
19+
resolve: () => ({})
20+
})

‎packages/server/src/graphql/auth.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { mutationField, stringArg, objectType } from "yoga";
2+
import * as jwt from 'jsonwebtoken'
3+
import { getGithubToken, getGithubUser, GithubUser } from '../github';
4+
import { Context } from "../context";
5+
import { User } from "../../.yoga/prisma-client";
6+
import config from "../config";
7+
8+
export const authenticate = mutationField("authenticate", {
9+
type: "AuthenticateUserPayload",
10+
nullable: true,
11+
args: {
12+
githubCode: stringArg({
13+
required: true,
14+
description: "GitHub OAuth Token from the client."
15+
})
16+
},
17+
resolve: async (_, { githubCode }, ctx) => {
18+
const githubToken = await getGithubToken(githubCode)
19+
const githubUser = await getGithubUser(githubToken)
20+
let user = await getPrismaUser(ctx, githubUser.id.toString())
21+
22+
if (!user) {
23+
user = await createPrismaUser(ctx, githubUser)
24+
}
25+
26+
return {
27+
token: jwt.sign({ userId: user.id }, config.jwt.SECRET),
28+
user
29+
}
30+
}
31+
})
32+
33+
export const AuthenticateUserPayload = objectType({
34+
name: "AuthenticateUserPayload",
35+
definition: (t) => {
36+
t.field("user", {
37+
type: "User"
38+
})
39+
t.string("token")
40+
}
41+
})
42+
// Helpers -------------------------------------------------------------------
43+
44+
async function getPrismaUser(ctx: Context, githubUserId: string): Promise<User> {
45+
return await ctx.prisma.user({ githubUserId })
46+
}
47+
48+
async function createPrismaUser(ctx: Context, githubUser: GithubUser): Promise<User> {
49+
const user = await ctx.prisma.createUser({
50+
githubUserId: githubUser.id.toString(),
51+
name: githubUser.name,
52+
email: githubUser.email,
53+
githubHandle: githubUser.login,
54+
bio: githubUser.bio,
55+
avatarUrl: githubUser.avatar_url
56+
})
57+
return user
58+
}

‎packages/server/src/schema.graphql

+95-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
### This file was autogenerated by Nexus 0.9.17
1+
### This file was autogenerated by Nexus 0.11.3
22
### Do not make changes to this file directly
33

44

5+
type AuthenticateUserPayload {
6+
token: String!
7+
user: User!
8+
}
9+
10+
scalar DateTime
11+
512
type Mutation {
13+
authenticate(
14+
"""GitHub OAuth Token from the client."""
15+
githubCode: String!
16+
): AuthenticateUserPayload
617
createDraft(authorEmail: String!, content: String!, title: String!): Post!
718
deletePost(id: ID!): Post
819
publish(id: ID!): Post!
@@ -86,17 +97,60 @@ input PostWhereInput {
8697
type Query {
8798
feed: [Post!]!
8899
filterPosts(searchString: String!): [Post!]!
100+
viewer: Viewer!
89101
}
90102

91103
type User {
104+
avatarUrl: String
105+
bio: String!
106+
createdAt: DateTime!
92107
email: String!
108+
githubHandle: String!
109+
githubUserId: String!
93110
id: ID!
94111
name: String!
95112
posts(after: String, before: String, first: Int, last: Int, orderBy: PostOrderByInput, skip: Int, where: PostWhereInput): [Post!]
113+
updatedAt: DateTime!
96114
}
97115

98116
input UserWhereInput {
99117
AND: [UserWhereInput!]
118+
avatarUrl: String
119+
avatarUrl_contains: String
120+
avatarUrl_ends_with: String
121+
avatarUrl_gt: String
122+
avatarUrl_gte: String
123+
avatarUrl_in: [String!]
124+
avatarUrl_lt: String
125+
avatarUrl_lte: String
126+
avatarUrl_not: String
127+
avatarUrl_not_contains: String
128+
avatarUrl_not_ends_with: String
129+
avatarUrl_not_in: [String!]
130+
avatarUrl_not_starts_with: String
131+
avatarUrl_starts_with: String
132+
bio: String
133+
bio_contains: String
134+
bio_ends_with: String
135+
bio_gt: String
136+
bio_gte: String
137+
bio_in: [String!]
138+
bio_lt: String
139+
bio_lte: String
140+
bio_not: String
141+
bio_not_contains: String
142+
bio_not_ends_with: String
143+
bio_not_in: [String!]
144+
bio_not_starts_with: String
145+
bio_starts_with: String
146+
createdAt: DateTime
147+
createdAt_gt: DateTime
148+
createdAt_gte: DateTime
149+
createdAt_in: [DateTime!]
150+
createdAt_lt: DateTime
151+
createdAt_lte: DateTime
152+
createdAt_not: DateTime
153+
createdAt_not_in: [DateTime!]
100154
email: String
101155
email_contains: String
102156
email_ends_with: String
@@ -111,6 +165,34 @@ input UserWhereInput {
111165
email_not_in: [String!]
112166
email_not_starts_with: String
113167
email_starts_with: String
168+
githubHandle: String
169+
githubHandle_contains: String
170+
githubHandle_ends_with: String
171+
githubHandle_gt: String
172+
githubHandle_gte: String
173+
githubHandle_in: [String!]
174+
githubHandle_lt: String
175+
githubHandle_lte: String
176+
githubHandle_not: String
177+
githubHandle_not_contains: String
178+
githubHandle_not_ends_with: String
179+
githubHandle_not_in: [String!]
180+
githubHandle_not_starts_with: String
181+
githubHandle_starts_with: String
182+
githubUserId: String
183+
githubUserId_contains: String
184+
githubUserId_ends_with: String
185+
githubUserId_gt: String
186+
githubUserId_gte: String
187+
githubUserId_in: [String!]
188+
githubUserId_lt: String
189+
githubUserId_lte: String
190+
githubUserId_not: String
191+
githubUserId_not_contains: String
192+
githubUserId_not_ends_with: String
193+
githubUserId_not_in: [String!]
194+
githubUserId_not_starts_with: String
195+
githubUserId_starts_with: String
114196
id: ID
115197
id_contains: ID
116198
id_ends_with: ID
@@ -144,4 +226,16 @@ input UserWhereInput {
144226
posts_every: PostWhereInput
145227
posts_none: PostWhereInput
146228
posts_some: PostWhereInput
229+
updatedAt: DateTime
230+
updatedAt_gt: DateTime
231+
updatedAt_gte: DateTime
232+
updatedAt_in: [DateTime!]
233+
updatedAt_lt: DateTime
234+
updatedAt_lte: DateTime
235+
updatedAt_not: DateTime
236+
updatedAt_not_in: [DateTime!]
237+
}
238+
239+
type Viewer {
240+
user: User!
147241
}

‎packages/server/src/utils.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as jwt from 'jsonwebtoken'
2+
import { Context } from "./context";
3+
import config from './config';
4+
5+
export function getUserId(ctx: Context) {
6+
const Authorization = ctx.req.get('Authorization')
7+
if (Authorization) {
8+
const token = Authorization.replace('Bearer ', '')
9+
const { userId } = jwt.verify(token, config.jwt.SECRET!) as {
10+
userId: string
11+
}
12+
return userId
13+
}
14+
15+
throw new AuthError()
16+
}
17+
18+
export class AuthError extends Error {
19+
constructor() {
20+
super('Not authorized')
21+
}
22+
}

0 commit comments

Comments
 (0)