@appforgeapps/shieldforge-graphql 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/dist/documents.d.ts +23 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +123 -0
- package/dist/documents.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/resolvers.d.ts +81 -0
- package/dist/resolvers.d.ts.map +1 -0
- package/dist/resolvers.js +128 -0
- package/dist/resolvers.js.map +1 -0
- package/dist/typeDefs.d.ts +5 -0
- package/dist/typeDefs.d.ts.map +1 -0
- package/dist/typeDefs.js +71 -0
- package/dist/typeDefs.js.map +1 -0
- package/package.json +47 -0
- package/src/documents.ts +133 -0
- package/src/index.ts +29 -0
- package/src/resolvers.ts +221 -0
- package/src/typeDefs.ts +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @shieldforge/graphql
|
|
2
|
+
|
|
3
|
+
GraphQL schema definitions and resolvers for authentication.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @shieldforge/graphql
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies:
|
|
12
|
+
```bash
|
|
13
|
+
npm install graphql
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Backend Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createResolvers, typeDefs } from '@shieldforge/graphql';
|
|
22
|
+
import { ShieldForge } from '@shieldforge/core';
|
|
23
|
+
|
|
24
|
+
const auth = new ShieldForge({
|
|
25
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Implement data source
|
|
29
|
+
const dataSource = {
|
|
30
|
+
getUserById: async (id) => await db.user.findUnique({ where: { id } }),
|
|
31
|
+
getUserByEmail: async (email) => await db.user.findUnique({ where: { email } }),
|
|
32
|
+
createUser: async (input) => await db.user.create({ data: input }),
|
|
33
|
+
updateUser: async (id, input) => await db.user.update({ where: { id }, data: input }),
|
|
34
|
+
createPasswordReset: async (userId, code, expiresAt) => {
|
|
35
|
+
await db.passwordReset.create({ data: { userId, code, expiresAt } });
|
|
36
|
+
},
|
|
37
|
+
getPasswordReset: async (code) => {
|
|
38
|
+
return await db.passwordReset.findUnique({ where: { code } });
|
|
39
|
+
},
|
|
40
|
+
deletePasswordReset: async (code) => {
|
|
41
|
+
await db.passwordReset.delete({ where: { code } });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Create resolvers
|
|
46
|
+
const resolvers = createResolvers({
|
|
47
|
+
dataSource,
|
|
48
|
+
auth: {
|
|
49
|
+
hashPassword: (password) => auth.hashPassword(password),
|
|
50
|
+
verifyPassword: (password, hash) => auth.verifyPassword(password, hash),
|
|
51
|
+
generateToken: (payload) => auth.generateToken(payload),
|
|
52
|
+
verifyToken: (token) => auth.verifyToken(token),
|
|
53
|
+
calculatePasswordStrength: (password) => auth.calculatePasswordStrength(password),
|
|
54
|
+
sanitizeUser: (user) => auth.sanitizeUser(user),
|
|
55
|
+
generateResetCode: () => auth.generateResetCode(),
|
|
56
|
+
sendPasswordResetEmail: (to, code) => auth.sendPasswordResetEmail(to, code),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Use with Apollo Server
|
|
61
|
+
import { ApolloServer } from '@apollo/server';
|
|
62
|
+
|
|
63
|
+
const server = new ApolloServer({
|
|
64
|
+
typeDefs,
|
|
65
|
+
resolvers,
|
|
66
|
+
context: ({ req }) => {
|
|
67
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
68
|
+
if (token) {
|
|
69
|
+
try {
|
|
70
|
+
const payload = auth.verifyToken(token);
|
|
71
|
+
return { userId: payload.userId, token };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Frontend Usage
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { LOGIN_MUTATION, REGISTER_MUTATION, ME_QUERY } from '@shieldforge/graphql';
|
|
85
|
+
import { useMutation, useQuery } from '@apollo/client';
|
|
86
|
+
import { useAuth } from '@shieldforge/react';
|
|
87
|
+
|
|
88
|
+
function LoginForm() {
|
|
89
|
+
const [login, { loading, error }] = useMutation(LOGIN_MUTATION);
|
|
90
|
+
const { login: authLogin } = useAuth();
|
|
91
|
+
|
|
92
|
+
const handleSubmit = async (email: string, password: string) => {
|
|
93
|
+
const { data } = await login({
|
|
94
|
+
variables: { input: { email, password } }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
authLogin(data.login.token, data.login.user);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (/* your form */);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Profile() {
|
|
104
|
+
const { data, loading } = useQuery(ME_QUERY);
|
|
105
|
+
|
|
106
|
+
if (loading) return <div>Loading...</div>;
|
|
107
|
+
|
|
108
|
+
return <div>Email: {data.me.email}</div>;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Type Definitions
|
|
113
|
+
|
|
114
|
+
The package exports complete GraphQL schema:
|
|
115
|
+
|
|
116
|
+
- `User` type
|
|
117
|
+
- `AuthPayload` type
|
|
118
|
+
- `LoginInput`, `RegisterInput`, `UpdateProfileInput`, `UpdatePasswordInput`
|
|
119
|
+
- `Query.me` - Get current user
|
|
120
|
+
- `Query.checkPasswordStrength` - Check password strength
|
|
121
|
+
- `Mutation.login` - Login user
|
|
122
|
+
- `Mutation.register` - Register user
|
|
123
|
+
- `Mutation.logout` - Logout user
|
|
124
|
+
- `Mutation.updateProfile` - Update user profile
|
|
125
|
+
- `Mutation.updatePassword` - Change password
|
|
126
|
+
- `Mutation.requestPasswordReset` - Request password reset
|
|
127
|
+
- `Mutation.resetPassword` - Reset password with code
|
|
128
|
+
|
|
129
|
+
## Documents
|
|
130
|
+
|
|
131
|
+
Pre-built query/mutation strings:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import {
|
|
135
|
+
LOGIN_MUTATION,
|
|
136
|
+
REGISTER_MUTATION,
|
|
137
|
+
LOGOUT_MUTATION,
|
|
138
|
+
ME_QUERY,
|
|
139
|
+
UPDATE_PROFILE_MUTATION,
|
|
140
|
+
UPDATE_PASSWORD_MUTATION,
|
|
141
|
+
REQUEST_PASSWORD_RESET_MUTATION,
|
|
142
|
+
RESET_PASSWORD_MUTATION,
|
|
143
|
+
CHECK_PASSWORD_STRENGTH_QUERY,
|
|
144
|
+
USER_FIELDS_FRAGMENT,
|
|
145
|
+
AUTH_PAYLOAD_FRAGMENT,
|
|
146
|
+
} from '@shieldforge/graphql';
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Extending the Schema
|
|
150
|
+
|
|
151
|
+
You can extend the User type with your own fields:
|
|
152
|
+
|
|
153
|
+
```graphql
|
|
154
|
+
extend type User {
|
|
155
|
+
customField: String
|
|
156
|
+
anotherField: Int
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then update your data source to return the additional fields.
|
|
161
|
+
|
|
162
|
+
## Data Source Interface
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
interface AuthDataSource {
|
|
166
|
+
getUserById(id: string): Promise<User | null>;
|
|
167
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
168
|
+
createUser(input: RegisterInput & { passwordHash: string }): Promise<User>;
|
|
169
|
+
updateUser(id: string, input: Partial<User>): Promise<User>;
|
|
170
|
+
createPasswordReset(userId: string, code: string, expiresAt: Date): Promise<void>;
|
|
171
|
+
getPasswordReset(code: string): Promise<{ userId: string; expiresAt: Date } | null>;
|
|
172
|
+
deletePasswordReset(code: string): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL query and mutation documents for Apollo Client
|
|
3
|
+
* These can be used directly with Apollo Client's useQuery and useMutation hooks
|
|
4
|
+
*/
|
|
5
|
+
export declare const LOGIN_MUTATION = "\n mutation Login($input: LoginInput!) {\n login(input: $input) {\n user {\n id\n email\n username\n name\n accountStatus\n emailVerified\n createdAt\n updatedAt\n }\n token\n }\n }\n";
|
|
6
|
+
export declare const REGISTER_MUTATION = "\n mutation Register($input: RegisterInput!) {\n register(input: $input) {\n user {\n id\n email\n username\n name\n accountStatus\n emailVerified\n createdAt\n updatedAt\n }\n token\n }\n }\n";
|
|
7
|
+
export declare const LOGOUT_MUTATION = "\n mutation Logout {\n logout\n }\n";
|
|
8
|
+
export declare const ME_QUERY = "\n query Me {\n me {\n id\n email\n username\n name\n accountStatus\n emailVerified\n createdAt\n updatedAt\n }\n }\n";
|
|
9
|
+
export declare const UPDATE_PROFILE_MUTATION = "\n mutation UpdateProfile($input: UpdateProfileInput!) {\n updateProfile(input: $input) {\n id\n email\n username\n name\n accountStatus\n emailVerified\n createdAt\n updatedAt\n }\n }\n";
|
|
10
|
+
export declare const UPDATE_PASSWORD_MUTATION = "\n mutation UpdatePassword($input: UpdatePasswordInput!) {\n updatePassword(input: $input)\n }\n";
|
|
11
|
+
export declare const REQUEST_PASSWORD_RESET_MUTATION = "\n mutation RequestPasswordReset($email: String!) {\n requestPasswordReset(email: $email)\n }\n";
|
|
12
|
+
export declare const RESET_PASSWORD_MUTATION = "\n mutation ResetPassword($code: String!, $newPassword: String!) {\n resetPassword(code: $code, newPassword: $newPassword)\n }\n";
|
|
13
|
+
export declare const CHECK_PASSWORD_STRENGTH_QUERY = "\n query CheckPasswordStrength($password: String!) {\n checkPasswordStrength(password: $password) {\n score\n feedback\n }\n }\n";
|
|
14
|
+
/**
|
|
15
|
+
* Fragment for User fields
|
|
16
|
+
* Can be composed into other queries
|
|
17
|
+
*/
|
|
18
|
+
export declare const USER_FIELDS_FRAGMENT = "\n fragment UserFields on User {\n id\n email\n username\n name\n accountStatus\n emailVerified\n createdAt\n updatedAt\n }\n";
|
|
19
|
+
/**
|
|
20
|
+
* Fragment for AuthPayload fields
|
|
21
|
+
*/
|
|
22
|
+
export declare const AUTH_PAYLOAD_FRAGMENT = "\n fragment AuthPayloadFields on AuthPayload {\n user {\n ...UserFields\n }\n token\n }\n \n fragment UserFields on User {\n id\n email\n username\n name\n accountStatus\n emailVerified\n createdAt\n updatedAt\n }\n\n";
|
|
23
|
+
//# sourceMappingURL=documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,cAAc,2QAgB1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,oRAgB7B,CAAC;AAEF,eAAO,MAAM,eAAe,6CAI3B,CAAC;AAEF,eAAO,MAAM,QAAQ,0KAapB,CAAC;AAEF,eAAO,MAAM,uBAAuB,+OAanC,CAAC;AAEF,eAAO,MAAM,wBAAwB,0GAIpC,CAAC;AAEF,eAAO,MAAM,+BAA+B,yGAI3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,0IAInC,CAAC;AAEF,eAAO,MAAM,6BAA6B,uJAOzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,4JAWhC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,yQAQjC,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL query and mutation documents for Apollo Client
|
|
3
|
+
* These can be used directly with Apollo Client's useQuery and useMutation hooks
|
|
4
|
+
*/
|
|
5
|
+
export const LOGIN_MUTATION = `
|
|
6
|
+
mutation Login($input: LoginInput!) {
|
|
7
|
+
login(input: $input) {
|
|
8
|
+
user {
|
|
9
|
+
id
|
|
10
|
+
email
|
|
11
|
+
username
|
|
12
|
+
name
|
|
13
|
+
accountStatus
|
|
14
|
+
emailVerified
|
|
15
|
+
createdAt
|
|
16
|
+
updatedAt
|
|
17
|
+
}
|
|
18
|
+
token
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
export const REGISTER_MUTATION = `
|
|
23
|
+
mutation Register($input: RegisterInput!) {
|
|
24
|
+
register(input: $input) {
|
|
25
|
+
user {
|
|
26
|
+
id
|
|
27
|
+
email
|
|
28
|
+
username
|
|
29
|
+
name
|
|
30
|
+
accountStatus
|
|
31
|
+
emailVerified
|
|
32
|
+
createdAt
|
|
33
|
+
updatedAt
|
|
34
|
+
}
|
|
35
|
+
token
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
export const LOGOUT_MUTATION = `
|
|
40
|
+
mutation Logout {
|
|
41
|
+
logout
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
export const ME_QUERY = `
|
|
45
|
+
query Me {
|
|
46
|
+
me {
|
|
47
|
+
id
|
|
48
|
+
email
|
|
49
|
+
username
|
|
50
|
+
name
|
|
51
|
+
accountStatus
|
|
52
|
+
emailVerified
|
|
53
|
+
createdAt
|
|
54
|
+
updatedAt
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
export const UPDATE_PROFILE_MUTATION = `
|
|
59
|
+
mutation UpdateProfile($input: UpdateProfileInput!) {
|
|
60
|
+
updateProfile(input: $input) {
|
|
61
|
+
id
|
|
62
|
+
email
|
|
63
|
+
username
|
|
64
|
+
name
|
|
65
|
+
accountStatus
|
|
66
|
+
emailVerified
|
|
67
|
+
createdAt
|
|
68
|
+
updatedAt
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
export const UPDATE_PASSWORD_MUTATION = `
|
|
73
|
+
mutation UpdatePassword($input: UpdatePasswordInput!) {
|
|
74
|
+
updatePassword(input: $input)
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
export const REQUEST_PASSWORD_RESET_MUTATION = `
|
|
78
|
+
mutation RequestPasswordReset($email: String!) {
|
|
79
|
+
requestPasswordReset(email: $email)
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
export const RESET_PASSWORD_MUTATION = `
|
|
83
|
+
mutation ResetPassword($code: String!, $newPassword: String!) {
|
|
84
|
+
resetPassword(code: $code, newPassword: $newPassword)
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
export const CHECK_PASSWORD_STRENGTH_QUERY = `
|
|
88
|
+
query CheckPasswordStrength($password: String!) {
|
|
89
|
+
checkPasswordStrength(password: $password) {
|
|
90
|
+
score
|
|
91
|
+
feedback
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
/**
|
|
96
|
+
* Fragment for User fields
|
|
97
|
+
* Can be composed into other queries
|
|
98
|
+
*/
|
|
99
|
+
export const USER_FIELDS_FRAGMENT = `
|
|
100
|
+
fragment UserFields on User {
|
|
101
|
+
id
|
|
102
|
+
email
|
|
103
|
+
username
|
|
104
|
+
name
|
|
105
|
+
accountStatus
|
|
106
|
+
emailVerified
|
|
107
|
+
createdAt
|
|
108
|
+
updatedAt
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
/**
|
|
112
|
+
* Fragment for AuthPayload fields
|
|
113
|
+
*/
|
|
114
|
+
export const AUTH_PAYLOAD_FRAGMENT = `
|
|
115
|
+
fragment AuthPayloadFields on AuthPayload {
|
|
116
|
+
user {
|
|
117
|
+
...UserFields
|
|
118
|
+
}
|
|
119
|
+
token
|
|
120
|
+
}
|
|
121
|
+
${USER_FIELDS_FRAGMENT}
|
|
122
|
+
`;
|
|
123
|
+
//# sourceMappingURL=documents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;CAgB7B,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;CAgBhC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;CAI9B,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;CAavB,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;CAatC,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG;;;;CAIvC,CAAC;AAEF,MAAM,CAAC,MAAM,+BAA+B,GAAG;;;;CAI9C,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;CAItC,CAAC;AAEF,MAAM,CAAC,MAAM,6BAA6B,GAAG;;;;;;;CAO5C,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;CAWnC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;IAOjC,oBAAoB;CACvB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { typeDefs } from './typeDefs';
|
|
2
|
+
export { createResolvers } from './resolvers';
|
|
3
|
+
export type { AuthDataSource, AuthContext, ResolverDependencies } from './resolvers';
|
|
4
|
+
export { LOGIN_MUTATION, REGISTER_MUTATION, LOGOUT_MUTATION, ME_QUERY, UPDATE_PROFILE_MUTATION, UPDATE_PASSWORD_MUTATION, REQUEST_PASSWORD_RESET_MUTATION, RESET_PASSWORD_MUTATION, CHECK_PASSWORD_STRENGTH_QUERY, USER_FIELDS_FRAGMENT, AUTH_PAYLOAD_FRAGMENT, } from './documents';
|
|
5
|
+
export type { User, AuthUser, LoginInput, RegisterInput, UpdateProfileInput, UpdatePasswordInput, AuthPayload, PasswordStrength, } from '@appforgeapps/shieldforge-types';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAErF,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAC/B,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,gBAAgB,GACjB,MAAM,iCAAiC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { typeDefs } from './typeDefs';
|
|
2
|
+
export { createResolvers } from './resolvers';
|
|
3
|
+
export { LOGIN_MUTATION, REGISTER_MUTATION, LOGOUT_MUTATION, ME_QUERY, UPDATE_PROFILE_MUTATION, UPDATE_PASSWORD_MUTATION, REQUEST_PASSWORD_RESET_MUTATION, RESET_PASSWORD_MUTATION, CHECK_PASSWORD_STRENGTH_QUERY, USER_FIELDS_FRAGMENT, AUTH_PAYLOAD_FRAGMENT, } from './documents';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAC/B,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { User, AuthUser, LoginInput, RegisterInput, UpdateProfileInput, UpdatePasswordInput, AuthPayload, PasswordStrength } from '@appforgeapps/shieldforge-types';
|
|
2
|
+
/**
|
|
3
|
+
* Data source functions that must be implemented by the consumer
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthDataSource {
|
|
6
|
+
getUserById(id: string): Promise<User | null>;
|
|
7
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
8
|
+
createUser(input: RegisterInput & {
|
|
9
|
+
passwordHash: string;
|
|
10
|
+
}): Promise<User>;
|
|
11
|
+
updateUser(id: string, input: Partial<User>): Promise<User>;
|
|
12
|
+
createPasswordReset(userId: string, code: string, expiresAt: Date): Promise<void>;
|
|
13
|
+
getPasswordReset(code: string): Promise<{
|
|
14
|
+
userId: string;
|
|
15
|
+
expiresAt: Date;
|
|
16
|
+
} | null>;
|
|
17
|
+
deletePasswordReset(code: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Context interface that must be provided to resolvers
|
|
21
|
+
*/
|
|
22
|
+
export interface AuthContext {
|
|
23
|
+
userId?: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolver dependencies
|
|
28
|
+
*/
|
|
29
|
+
export interface ResolverDependencies {
|
|
30
|
+
dataSource: AuthDataSource;
|
|
31
|
+
auth: {
|
|
32
|
+
hashPassword(password: string): Promise<string>;
|
|
33
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
34
|
+
generateToken(payload: {
|
|
35
|
+
userId: string;
|
|
36
|
+
email: string;
|
|
37
|
+
}): string;
|
|
38
|
+
verifyToken(token: string): {
|
|
39
|
+
userId: string;
|
|
40
|
+
email: string;
|
|
41
|
+
};
|
|
42
|
+
calculatePasswordStrength(password: string): PasswordStrength;
|
|
43
|
+
sanitizeUser(user: User): AuthUser;
|
|
44
|
+
generateResetCode(length?: number): string;
|
|
45
|
+
sendPasswordResetEmail(to: string, code: string, resetUrl?: string): Promise<void>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create GraphQL resolvers with dependency injection
|
|
50
|
+
*/
|
|
51
|
+
export declare function createResolvers(deps: ResolverDependencies): {
|
|
52
|
+
Query: {
|
|
53
|
+
me: (_parent: any, _args: any, context: AuthContext) => Promise<AuthUser | null>;
|
|
54
|
+
checkPasswordStrength: (_parent: any, args: {
|
|
55
|
+
password: string;
|
|
56
|
+
}) => PasswordStrength;
|
|
57
|
+
};
|
|
58
|
+
Mutation: {
|
|
59
|
+
login: (_parent: any, args: {
|
|
60
|
+
input: LoginInput;
|
|
61
|
+
}) => Promise<AuthPayload>;
|
|
62
|
+
register: (_parent: any, args: {
|
|
63
|
+
input: RegisterInput;
|
|
64
|
+
}) => Promise<AuthPayload>;
|
|
65
|
+
logout: (_parent: any, _args: any, _context: AuthContext) => boolean;
|
|
66
|
+
updateProfile: (_parent: any, args: {
|
|
67
|
+
input: UpdateProfileInput;
|
|
68
|
+
}, context: AuthContext) => Promise<AuthUser>;
|
|
69
|
+
updatePassword: (_parent: any, args: {
|
|
70
|
+
input: UpdatePasswordInput;
|
|
71
|
+
}, context: AuthContext) => Promise<boolean>;
|
|
72
|
+
requestPasswordReset: (_parent: any, args: {
|
|
73
|
+
email: string;
|
|
74
|
+
}) => Promise<boolean>;
|
|
75
|
+
resetPassword: (_parent: any, args: {
|
|
76
|
+
code: string;
|
|
77
|
+
newPassword: string;
|
|
78
|
+
}) => Promise<boolean>;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=resolvers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EACjB,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,MAAM,WAAW,cAAc;IAE7B,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC9C,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpD,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAG5D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACpF,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,cAAc,CAAC;IAC3B,IAAI,EAAE;QACJ,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAChD,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACjE,aAAa,CAAC,OAAO,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,GAAG,MAAM,CAAC;QAClE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QAC9D,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAAC;QAC9D,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC;QACnC,iBAAiB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC3C,sBAAsB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACpF,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,oBAAoB;;sBAKhC,GAAG,SAAS,GAAG,WAAW,WAAW,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;yCAQnD,GAAG,QAAQ;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAG,gBAAgB;;;yBAM5D,GAAG,QAAQ;YAAE,KAAK,EAAE,UAAU,CAAA;SAAE,KAAG,OAAO,CAAC,WAAW,CAAC;4BAqBpD,GAAG,QAAQ;YAAE,KAAK,EAAE,aAAa,CAAA;SAAE,KAAG,OAAO,CAAC,WAAW,CAAC;0BAiClE,GAAG,SAAS,GAAG,YAAY,WAAW,KAAG,OAAO;iCAOvD,GAAG,QACN;YAAE,KAAK,EAAE,kBAAkB,CAAA;SAAE,WAC1B,WAAW,KACnB,OAAO,CAAC,QAAQ,CAAC;kCAUT,GAAG,QACN;YAAE,KAAK,EAAE,mBAAmB,CAAA;SAAE,WAC3B,WAAW,KACnB,OAAO,CAAC,OAAO,CAAC;wCA0BmB,GAAG,QAAQ;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,KAAG,OAAO,CAAC,OAAO,CAAC;iCAiB1E,GAAG,QACN;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,KAC1C,OAAO,CAAC,OAAO,CAAC;;EAwBxB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create GraphQL resolvers with dependency injection
|
|
3
|
+
*/
|
|
4
|
+
export function createResolvers(deps) {
|
|
5
|
+
const { dataSource, auth } = deps;
|
|
6
|
+
return {
|
|
7
|
+
Query: {
|
|
8
|
+
me: async (_parent, _args, context) => {
|
|
9
|
+
if (!context.userId) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const user = await dataSource.getUserById(context.userId);
|
|
13
|
+
return user ? auth.sanitizeUser(user) : null;
|
|
14
|
+
},
|
|
15
|
+
checkPasswordStrength: (_parent, args) => {
|
|
16
|
+
return auth.calculatePasswordStrength(args.password);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
Mutation: {
|
|
20
|
+
login: async (_parent, args) => {
|
|
21
|
+
const { email, password } = args.input;
|
|
22
|
+
const user = await dataSource.getUserByEmail(email);
|
|
23
|
+
if (!user || !user.passwordHash) {
|
|
24
|
+
throw new Error('Invalid email or password');
|
|
25
|
+
}
|
|
26
|
+
const isValid = await auth.verifyPassword(password, user.passwordHash);
|
|
27
|
+
if (!isValid) {
|
|
28
|
+
throw new Error('Invalid email or password');
|
|
29
|
+
}
|
|
30
|
+
const token = auth.generateToken({ userId: user.id, email: user.email });
|
|
31
|
+
return {
|
|
32
|
+
user: auth.sanitizeUser(user),
|
|
33
|
+
token,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
register: async (_parent, args) => {
|
|
37
|
+
const { email, password, username, name } = args.input;
|
|
38
|
+
// Check if user already exists
|
|
39
|
+
const existingUser = await dataSource.getUserByEmail(email);
|
|
40
|
+
if (existingUser) {
|
|
41
|
+
throw new Error('User with this email already exists');
|
|
42
|
+
}
|
|
43
|
+
// Validate password strength
|
|
44
|
+
const strength = auth.calculatePasswordStrength(password);
|
|
45
|
+
if (strength.score < 2) {
|
|
46
|
+
throw new Error(`Password is too weak: ${strength.feedback.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
// Hash password and create user
|
|
49
|
+
const passwordHash = await auth.hashPassword(password);
|
|
50
|
+
const user = await dataSource.createUser({
|
|
51
|
+
email,
|
|
52
|
+
password,
|
|
53
|
+
username,
|
|
54
|
+
name,
|
|
55
|
+
passwordHash,
|
|
56
|
+
});
|
|
57
|
+
const token = auth.generateToken({ userId: user.id, email: user.email });
|
|
58
|
+
return {
|
|
59
|
+
user: auth.sanitizeUser(user),
|
|
60
|
+
token,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
logout: (_parent, _args, _context) => {
|
|
64
|
+
// Logout is typically handled client-side by removing the token
|
|
65
|
+
// Server-side can optionally invalidate the token if using a token blacklist
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
updateProfile: async (_parent, args, context) => {
|
|
69
|
+
if (!context.userId) {
|
|
70
|
+
throw new Error('Not authenticated');
|
|
71
|
+
}
|
|
72
|
+
const user = await dataSource.updateUser(context.userId, args.input);
|
|
73
|
+
return auth.sanitizeUser(user);
|
|
74
|
+
},
|
|
75
|
+
updatePassword: async (_parent, args, context) => {
|
|
76
|
+
if (!context.userId) {
|
|
77
|
+
throw new Error('Not authenticated');
|
|
78
|
+
}
|
|
79
|
+
const user = await dataSource.getUserById(context.userId);
|
|
80
|
+
if (!user || !user.passwordHash) {
|
|
81
|
+
throw new Error('User not found');
|
|
82
|
+
}
|
|
83
|
+
const isValid = await auth.verifyPassword(args.input.currentPassword, user.passwordHash);
|
|
84
|
+
if (!isValid) {
|
|
85
|
+
throw new Error('Current password is incorrect');
|
|
86
|
+
}
|
|
87
|
+
const strength = auth.calculatePasswordStrength(args.input.newPassword);
|
|
88
|
+
if (strength.score < 2) {
|
|
89
|
+
throw new Error(`New password is too weak: ${strength.feedback.join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
const newPasswordHash = await auth.hashPassword(args.input.newPassword);
|
|
92
|
+
await dataSource.updateUser(context.userId, { passwordHash: newPasswordHash });
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
requestPasswordReset: async (_parent, args) => {
|
|
96
|
+
const user = await dataSource.getUserByEmail(args.email);
|
|
97
|
+
if (!user) {
|
|
98
|
+
// Don't reveal whether the email exists
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const resetCode = auth.generateResetCode();
|
|
102
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
|
103
|
+
await dataSource.createPasswordReset(user.id, resetCode, expiresAt);
|
|
104
|
+
await auth.sendPasswordResetEmail(user.email, resetCode);
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
resetPassword: async (_parent, args) => {
|
|
108
|
+
const reset = await dataSource.getPasswordReset(args.code);
|
|
109
|
+
if (!reset) {
|
|
110
|
+
throw new Error('Invalid or expired reset code');
|
|
111
|
+
}
|
|
112
|
+
if (new Date() > reset.expiresAt) {
|
|
113
|
+
await dataSource.deletePasswordReset(args.code);
|
|
114
|
+
throw new Error('Reset code has expired');
|
|
115
|
+
}
|
|
116
|
+
const strength = auth.calculatePasswordStrength(args.newPassword);
|
|
117
|
+
if (strength.score < 2) {
|
|
118
|
+
throw new Error(`New password is too weak: ${strength.feedback.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
const newPasswordHash = await auth.hashPassword(args.newPassword);
|
|
121
|
+
await dataSource.updateUser(reset.userId, { passwordHash: newPasswordHash });
|
|
122
|
+
await dataSource.deletePasswordReset(args.code);
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=resolvers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL type definitions for authentication
|
|
3
|
+
*/
|
|
4
|
+
export declare const typeDefs = "\n enum UserAccountStatus {\n ACTIVE\n INACTIVE\n SUSPENDED\n PENDING\n }\n\n type User {\n id: ID!\n email: String!\n username: String\n name: String\n accountStatus: UserAccountStatus\n emailVerified: Boolean\n createdAt: String\n updatedAt: String\n }\n\n type AuthPayload {\n user: User!\n token: String!\n }\n\n input LoginInput {\n email: String!\n password: String!\n }\n\n input RegisterInput {\n email: String!\n password: String!\n username: String\n name: String\n }\n\n input UpdateProfileInput {\n username: String\n name: String\n email: String\n }\n\n input UpdatePasswordInput {\n currentPassword: String!\n newPassword: String!\n }\n\n type PasswordStrength {\n score: Int!\n feedback: [String!]!\n }\n\n type Query {\n me: User\n checkPasswordStrength(password: String!): PasswordStrength!\n }\n\n type Mutation {\n login(input: LoginInput!): AuthPayload!\n register(input: RegisterInput!): AuthPayload!\n logout: Boolean!\n updateProfile(input: UpdateProfileInput!): User!\n updatePassword(input: UpdatePasswordInput!): Boolean!\n requestPasswordReset(email: String!): Boolean!\n resetPassword(code: String!, newPassword: String!): Boolean!\n }\n";
|
|
5
|
+
//# sourceMappingURL=typeDefs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typeDefs.d.ts","sourceRoot":"","sources":["../src/typeDefs.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,QAAQ,kxCAkEpB,CAAC"}
|
package/dist/typeDefs.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL type definitions for authentication
|
|
3
|
+
*/
|
|
4
|
+
export const typeDefs = `
|
|
5
|
+
enum UserAccountStatus {
|
|
6
|
+
ACTIVE
|
|
7
|
+
INACTIVE
|
|
8
|
+
SUSPENDED
|
|
9
|
+
PENDING
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type User {
|
|
13
|
+
id: ID!
|
|
14
|
+
email: String!
|
|
15
|
+
username: String
|
|
16
|
+
name: String
|
|
17
|
+
accountStatus: UserAccountStatus
|
|
18
|
+
emailVerified: Boolean
|
|
19
|
+
createdAt: String
|
|
20
|
+
updatedAt: String
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type AuthPayload {
|
|
24
|
+
user: User!
|
|
25
|
+
token: String!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
input LoginInput {
|
|
29
|
+
email: String!
|
|
30
|
+
password: String!
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
input RegisterInput {
|
|
34
|
+
email: String!
|
|
35
|
+
password: String!
|
|
36
|
+
username: String
|
|
37
|
+
name: String
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
input UpdateProfileInput {
|
|
41
|
+
username: String
|
|
42
|
+
name: String
|
|
43
|
+
email: String
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
input UpdatePasswordInput {
|
|
47
|
+
currentPassword: String!
|
|
48
|
+
newPassword: String!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type PasswordStrength {
|
|
52
|
+
score: Int!
|
|
53
|
+
feedback: [String!]!
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Query {
|
|
57
|
+
me: User
|
|
58
|
+
checkPasswordStrength(password: String!): PasswordStrength!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Mutation {
|
|
62
|
+
login(input: LoginInput!): AuthPayload!
|
|
63
|
+
register(input: RegisterInput!): AuthPayload!
|
|
64
|
+
logout: Boolean!
|
|
65
|
+
updateProfile(input: UpdateProfileInput!): User!
|
|
66
|
+
updatePassword(input: UpdatePasswordInput!): Boolean!
|
|
67
|
+
requestPasswordReset(email: String!): Boolean!
|
|
68
|
+
resetPassword(code: String!, newPassword: String!): Boolean!
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
//# sourceMappingURL=typeDefs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typeDefs.js","sourceRoot":"","sources":["../src/typeDefs.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkEvB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appforgeapps/shieldforge-graphql",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "GraphQL schema definitions and resolvers for ShieldForge authentication",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/chriscase/ShieldForge.git",
|
|
8
|
+
"directory": "packages/graphql"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
23
|
+
"test": "echo \"No tests specified\"",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"graphql",
|
|
28
|
+
"authentication",
|
|
29
|
+
"auth",
|
|
30
|
+
"schema",
|
|
31
|
+
"resolvers",
|
|
32
|
+
"shieldforge"
|
|
33
|
+
],
|
|
34
|
+
"author": "chriscase",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@appforgeapps/shieldforge-types": "*"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"graphql": "^15.0.0 || ^16.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.10.0",
|
|
44
|
+
"graphql": "^16.8.1",
|
|
45
|
+
"typescript": "^5.3.3"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/documents.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL query and mutation documents for Apollo Client
|
|
3
|
+
* These can be used directly with Apollo Client's useQuery and useMutation hooks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LOGIN_MUTATION = `
|
|
7
|
+
mutation Login($input: LoginInput!) {
|
|
8
|
+
login(input: $input) {
|
|
9
|
+
user {
|
|
10
|
+
id
|
|
11
|
+
email
|
|
12
|
+
username
|
|
13
|
+
name
|
|
14
|
+
accountStatus
|
|
15
|
+
emailVerified
|
|
16
|
+
createdAt
|
|
17
|
+
updatedAt
|
|
18
|
+
}
|
|
19
|
+
token
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const REGISTER_MUTATION = `
|
|
25
|
+
mutation Register($input: RegisterInput!) {
|
|
26
|
+
register(input: $input) {
|
|
27
|
+
user {
|
|
28
|
+
id
|
|
29
|
+
email
|
|
30
|
+
username
|
|
31
|
+
name
|
|
32
|
+
accountStatus
|
|
33
|
+
emailVerified
|
|
34
|
+
createdAt
|
|
35
|
+
updatedAt
|
|
36
|
+
}
|
|
37
|
+
token
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const LOGOUT_MUTATION = `
|
|
43
|
+
mutation Logout {
|
|
44
|
+
logout
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export const ME_QUERY = `
|
|
49
|
+
query Me {
|
|
50
|
+
me {
|
|
51
|
+
id
|
|
52
|
+
email
|
|
53
|
+
username
|
|
54
|
+
name
|
|
55
|
+
accountStatus
|
|
56
|
+
emailVerified
|
|
57
|
+
createdAt
|
|
58
|
+
updatedAt
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
export const UPDATE_PROFILE_MUTATION = `
|
|
64
|
+
mutation UpdateProfile($input: UpdateProfileInput!) {
|
|
65
|
+
updateProfile(input: $input) {
|
|
66
|
+
id
|
|
67
|
+
email
|
|
68
|
+
username
|
|
69
|
+
name
|
|
70
|
+
accountStatus
|
|
71
|
+
emailVerified
|
|
72
|
+
createdAt
|
|
73
|
+
updatedAt
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
export const UPDATE_PASSWORD_MUTATION = `
|
|
79
|
+
mutation UpdatePassword($input: UpdatePasswordInput!) {
|
|
80
|
+
updatePassword(input: $input)
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export const REQUEST_PASSWORD_RESET_MUTATION = `
|
|
85
|
+
mutation RequestPasswordReset($email: String!) {
|
|
86
|
+
requestPasswordReset(email: $email)
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export const RESET_PASSWORD_MUTATION = `
|
|
91
|
+
mutation ResetPassword($code: String!, $newPassword: String!) {
|
|
92
|
+
resetPassword(code: $code, newPassword: $newPassword)
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
export const CHECK_PASSWORD_STRENGTH_QUERY = `
|
|
97
|
+
query CheckPasswordStrength($password: String!) {
|
|
98
|
+
checkPasswordStrength(password: $password) {
|
|
99
|
+
score
|
|
100
|
+
feedback
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fragment for User fields
|
|
107
|
+
* Can be composed into other queries
|
|
108
|
+
*/
|
|
109
|
+
export const USER_FIELDS_FRAGMENT = `
|
|
110
|
+
fragment UserFields on User {
|
|
111
|
+
id
|
|
112
|
+
email
|
|
113
|
+
username
|
|
114
|
+
name
|
|
115
|
+
accountStatus
|
|
116
|
+
emailVerified
|
|
117
|
+
createdAt
|
|
118
|
+
updatedAt
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fragment for AuthPayload fields
|
|
124
|
+
*/
|
|
125
|
+
export const AUTH_PAYLOAD_FRAGMENT = `
|
|
126
|
+
fragment AuthPayloadFields on AuthPayload {
|
|
127
|
+
user {
|
|
128
|
+
...UserFields
|
|
129
|
+
}
|
|
130
|
+
token
|
|
131
|
+
}
|
|
132
|
+
${USER_FIELDS_FRAGMENT}
|
|
133
|
+
`;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { typeDefs } from './typeDefs';
|
|
2
|
+
export { createResolvers } from './resolvers';
|
|
3
|
+
export type { AuthDataSource, AuthContext, ResolverDependencies } from './resolvers';
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
LOGIN_MUTATION,
|
|
7
|
+
REGISTER_MUTATION,
|
|
8
|
+
LOGOUT_MUTATION,
|
|
9
|
+
ME_QUERY,
|
|
10
|
+
UPDATE_PROFILE_MUTATION,
|
|
11
|
+
UPDATE_PASSWORD_MUTATION,
|
|
12
|
+
REQUEST_PASSWORD_RESET_MUTATION,
|
|
13
|
+
RESET_PASSWORD_MUTATION,
|
|
14
|
+
CHECK_PASSWORD_STRENGTH_QUERY,
|
|
15
|
+
USER_FIELDS_FRAGMENT,
|
|
16
|
+
AUTH_PAYLOAD_FRAGMENT,
|
|
17
|
+
} from './documents';
|
|
18
|
+
|
|
19
|
+
// Re-export types
|
|
20
|
+
export type {
|
|
21
|
+
User,
|
|
22
|
+
AuthUser,
|
|
23
|
+
LoginInput,
|
|
24
|
+
RegisterInput,
|
|
25
|
+
UpdateProfileInput,
|
|
26
|
+
UpdatePasswordInput,
|
|
27
|
+
AuthPayload,
|
|
28
|
+
PasswordStrength,
|
|
29
|
+
} from '@appforgeapps/shieldforge-types';
|
package/src/resolvers.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
User,
|
|
3
|
+
AuthUser,
|
|
4
|
+
LoginInput,
|
|
5
|
+
RegisterInput,
|
|
6
|
+
UpdateProfileInput,
|
|
7
|
+
UpdatePasswordInput,
|
|
8
|
+
AuthPayload,
|
|
9
|
+
PasswordStrength,
|
|
10
|
+
} from '@appforgeapps/shieldforge-types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Data source functions that must be implemented by the consumer
|
|
14
|
+
*/
|
|
15
|
+
export interface AuthDataSource {
|
|
16
|
+
// User operations
|
|
17
|
+
getUserById(id: string): Promise<User | null>;
|
|
18
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
19
|
+
createUser(input: RegisterInput & { passwordHash: string }): Promise<User>;
|
|
20
|
+
updateUser(id: string, input: Partial<User>): Promise<User>;
|
|
21
|
+
|
|
22
|
+
// Password reset operations
|
|
23
|
+
createPasswordReset(userId: string, code: string, expiresAt: Date): Promise<void>;
|
|
24
|
+
getPasswordReset(code: string): Promise<{ userId: string; expiresAt: Date } | null>;
|
|
25
|
+
deletePasswordReset(code: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Context interface that must be provided to resolvers
|
|
30
|
+
*/
|
|
31
|
+
export interface AuthContext {
|
|
32
|
+
userId?: string;
|
|
33
|
+
token?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolver dependencies
|
|
38
|
+
*/
|
|
39
|
+
export interface ResolverDependencies {
|
|
40
|
+
dataSource: AuthDataSource;
|
|
41
|
+
auth: {
|
|
42
|
+
hashPassword(password: string): Promise<string>;
|
|
43
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
44
|
+
generateToken(payload: { userId: string; email: string }): string;
|
|
45
|
+
verifyToken(token: string): { userId: string; email: string };
|
|
46
|
+
calculatePasswordStrength(password: string): PasswordStrength;
|
|
47
|
+
sanitizeUser(user: User): AuthUser;
|
|
48
|
+
generateResetCode(length?: number): string;
|
|
49
|
+
sendPasswordResetEmail(to: string, code: string, resetUrl?: string): Promise<void>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create GraphQL resolvers with dependency injection
|
|
55
|
+
*/
|
|
56
|
+
export function createResolvers(deps: ResolverDependencies) {
|
|
57
|
+
const { dataSource, auth } = deps;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
Query: {
|
|
61
|
+
me: async (_parent: any, _args: any, context: AuthContext): Promise<AuthUser | null> => {
|
|
62
|
+
if (!context.userId) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const user = await dataSource.getUserById(context.userId);
|
|
66
|
+
return user ? auth.sanitizeUser(user) : null;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
checkPasswordStrength: (_parent: any, args: { password: string }): PasswordStrength => {
|
|
70
|
+
return auth.calculatePasswordStrength(args.password);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
Mutation: {
|
|
75
|
+
login: async (_parent: any, args: { input: LoginInput }): Promise<AuthPayload> => {
|
|
76
|
+
const { email, password } = args.input;
|
|
77
|
+
|
|
78
|
+
const user = await dataSource.getUserByEmail(email);
|
|
79
|
+
if (!user || !user.passwordHash) {
|
|
80
|
+
throw new Error('Invalid email or password');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isValid = await auth.verifyPassword(password, user.passwordHash);
|
|
84
|
+
if (!isValid) {
|
|
85
|
+
throw new Error('Invalid email or password');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const token = auth.generateToken({ userId: user.id, email: user.email });
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
user: auth.sanitizeUser(user),
|
|
92
|
+
token,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
register: async (_parent: any, args: { input: RegisterInput }): Promise<AuthPayload> => {
|
|
97
|
+
const { email, password, username, name } = args.input;
|
|
98
|
+
|
|
99
|
+
// Check if user already exists
|
|
100
|
+
const existingUser = await dataSource.getUserByEmail(email);
|
|
101
|
+
if (existingUser) {
|
|
102
|
+
throw new Error('User with this email already exists');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate password strength
|
|
106
|
+
const strength = auth.calculatePasswordStrength(password);
|
|
107
|
+
if (strength.score < 2) {
|
|
108
|
+
throw new Error(`Password is too weak: ${strength.feedback.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Hash password and create user
|
|
112
|
+
const passwordHash = await auth.hashPassword(password);
|
|
113
|
+
const user = await dataSource.createUser({
|
|
114
|
+
email,
|
|
115
|
+
password,
|
|
116
|
+
username,
|
|
117
|
+
name,
|
|
118
|
+
passwordHash,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const token = auth.generateToken({ userId: user.id, email: user.email });
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
user: auth.sanitizeUser(user),
|
|
125
|
+
token,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
logout: (_parent: any, _args: any, _context: AuthContext): boolean => {
|
|
130
|
+
// Logout is typically handled client-side by removing the token
|
|
131
|
+
// Server-side can optionally invalidate the token if using a token blacklist
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
updateProfile: async (
|
|
136
|
+
_parent: any,
|
|
137
|
+
args: { input: UpdateProfileInput },
|
|
138
|
+
context: AuthContext
|
|
139
|
+
): Promise<AuthUser> => {
|
|
140
|
+
if (!context.userId) {
|
|
141
|
+
throw new Error('Not authenticated');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const user = await dataSource.updateUser(context.userId, args.input);
|
|
145
|
+
return auth.sanitizeUser(user);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
updatePassword: async (
|
|
149
|
+
_parent: any,
|
|
150
|
+
args: { input: UpdatePasswordInput },
|
|
151
|
+
context: AuthContext
|
|
152
|
+
): Promise<boolean> => {
|
|
153
|
+
if (!context.userId) {
|
|
154
|
+
throw new Error('Not authenticated');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const user = await dataSource.getUserById(context.userId);
|
|
158
|
+
if (!user || !user.passwordHash) {
|
|
159
|
+
throw new Error('User not found');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const isValid = await auth.verifyPassword(args.input.currentPassword, user.passwordHash);
|
|
163
|
+
if (!isValid) {
|
|
164
|
+
throw new Error('Current password is incorrect');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const strength = auth.calculatePasswordStrength(args.input.newPassword);
|
|
168
|
+
if (strength.score < 2) {
|
|
169
|
+
throw new Error(`New password is too weak: ${strength.feedback.join(', ')}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const newPasswordHash = await auth.hashPassword(args.input.newPassword);
|
|
173
|
+
await dataSource.updateUser(context.userId, { passwordHash: newPasswordHash });
|
|
174
|
+
|
|
175
|
+
return true;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
requestPasswordReset: async (_parent: any, args: { email: string }): Promise<boolean> => {
|
|
179
|
+
const user = await dataSource.getUserByEmail(args.email);
|
|
180
|
+
if (!user) {
|
|
181
|
+
// Don't reveal whether the email exists
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const resetCode = auth.generateResetCode();
|
|
186
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
|
187
|
+
|
|
188
|
+
await dataSource.createPasswordReset(user.id, resetCode, expiresAt);
|
|
189
|
+
await auth.sendPasswordResetEmail(user.email, resetCode);
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
resetPassword: async (
|
|
195
|
+
_parent: any,
|
|
196
|
+
args: { code: string; newPassword: string }
|
|
197
|
+
): Promise<boolean> => {
|
|
198
|
+
const reset = await dataSource.getPasswordReset(args.code);
|
|
199
|
+
if (!reset) {
|
|
200
|
+
throw new Error('Invalid or expired reset code');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (new Date() > reset.expiresAt) {
|
|
204
|
+
await dataSource.deletePasswordReset(args.code);
|
|
205
|
+
throw new Error('Reset code has expired');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const strength = auth.calculatePasswordStrength(args.newPassword);
|
|
209
|
+
if (strength.score < 2) {
|
|
210
|
+
throw new Error(`New password is too weak: ${strength.feedback.join(', ')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const newPasswordHash = await auth.hashPassword(args.newPassword);
|
|
214
|
+
await dataSource.updateUser(reset.userId, { passwordHash: newPasswordHash });
|
|
215
|
+
await dataSource.deletePasswordReset(args.code);
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/typeDefs.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL type definitions for authentication
|
|
3
|
+
*/
|
|
4
|
+
export const typeDefs = `
|
|
5
|
+
enum UserAccountStatus {
|
|
6
|
+
ACTIVE
|
|
7
|
+
INACTIVE
|
|
8
|
+
SUSPENDED
|
|
9
|
+
PENDING
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type User {
|
|
13
|
+
id: ID!
|
|
14
|
+
email: String!
|
|
15
|
+
username: String
|
|
16
|
+
name: String
|
|
17
|
+
accountStatus: UserAccountStatus
|
|
18
|
+
emailVerified: Boolean
|
|
19
|
+
createdAt: String
|
|
20
|
+
updatedAt: String
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type AuthPayload {
|
|
24
|
+
user: User!
|
|
25
|
+
token: String!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
input LoginInput {
|
|
29
|
+
email: String!
|
|
30
|
+
password: String!
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
input RegisterInput {
|
|
34
|
+
email: String!
|
|
35
|
+
password: String!
|
|
36
|
+
username: String
|
|
37
|
+
name: String
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
input UpdateProfileInput {
|
|
41
|
+
username: String
|
|
42
|
+
name: String
|
|
43
|
+
email: String
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
input UpdatePasswordInput {
|
|
47
|
+
currentPassword: String!
|
|
48
|
+
newPassword: String!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type PasswordStrength {
|
|
52
|
+
score: Int!
|
|
53
|
+
feedback: [String!]!
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Query {
|
|
57
|
+
me: User
|
|
58
|
+
checkPasswordStrength(password: String!): PasswordStrength!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Mutation {
|
|
62
|
+
login(input: LoginInput!): AuthPayload!
|
|
63
|
+
register(input: RegisterInput!): AuthPayload!
|
|
64
|
+
logout: Boolean!
|
|
65
|
+
updateProfile(input: UpdateProfileInput!): User!
|
|
66
|
+
updatePassword(input: UpdatePasswordInput!): Boolean!
|
|
67
|
+
requestPasswordReset(email: String!): Boolean!
|
|
68
|
+
resetPassword(code: String!, newPassword: String!): Boolean!
|
|
69
|
+
}
|
|
70
|
+
`;
|