Skip to content

Commit 3df5437

Browse files
committed
implement authentication and user management services
1 parent 82fb0f1 commit 3df5437

File tree

20 files changed

+2868
-58
lines changed

20 files changed

+2868
-58
lines changed

backend/.env.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
# Server Configuration
2+
PORT=3000
3+
NODE_ENV=development
4+
5+
# Database Configuration
6+
POSTGRES_USER=postgres
7+
POSTGRES_PASSWORD=password
8+
POSTGRES_DB_DEV=starter_dev
9+
POSTGRES_DB_TEST=starter_test
10+
DB_HOST=localhost
11+
12+
# Firebase Configuration
13+
FIREBASE_PROJECT_ID=your-project-id
14+
FIREBASE_SVC_ACCOUNT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYourPrivateKeyHere\n-----END PRIVATE KEY-----\n"
15+
FIREBASE_SVC_ACCOUNT_CLIENT_EMAIL=firebase-adminsdk@your-project-id.iam.gserviceaccount.com
16+
17+
# Firebase Web API Key
18+
FIREBASE_WEB_API_KEY=your-web-api-key
19+
20+
# Firebase Request URI (update for production)
21+
FIREBASE_REQUEST_URI=http://localhost
22+
23+
# Email Configuration
124
MAILER_USER=your-email@gmail.com
225
MAILER_REFRESH_TOKEN=your-refresh-token
326
MAILER_CLIENT_SECRET=your-client-secret

backend/graphql/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { merge } from 'lodash';
44

55
import sampleResolvers from './resolvers/sampleResolvers';
66
import sampleType from './types/sampleType';
7+
import userResolvers from './resolvers/userResolvers';
8+
import userType from './types/userType';
9+
import authResolvers from './resolvers/authResolvers';
10+
import authType from './types/authType';
711

812
const query = gql`
913
type Query {
@@ -18,8 +22,8 @@ const mutation = gql`
1822
`;
1923

2024
const executableSchema = makeExecutableSchema({
21-
typeDefs: [query, mutation, sampleType],
22-
resolvers: merge(sampleResolvers),
25+
typeDefs: [query, mutation, sampleType, userType, authType],
26+
resolvers: merge(sampleResolvers, userResolvers, authResolvers),
2327
});
2428

2529
export default executableSchema;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import nodemailerConfig from '../../nodemailer.config';
2+
import AuthService from '../../services/implementations/authService';
3+
import EmailService from '../../services/implementations/emailService';
4+
import UserService from '../../services/implementations/userService';
5+
import IAuthService from '../../services/interfaces/authService';
6+
import IEmailService from '../../services/interfaces/emailService';
7+
import IUserService from '../../services/interfaces/userService';
8+
import { AuthDTO, RegisterUserDTO, Role, SignUpMethod } from '../../types';
9+
10+
const userService: IUserService = new UserService();
11+
const emailService: IEmailService = new EmailService(nodemailerConfig);
12+
const authService: IAuthService = new AuthService(userService, emailService);
13+
14+
const authResolvers = {
15+
Query: {
16+
isAuthorizedByRole: async (
17+
_parent: undefined,
18+
{ accessToken, roles }: { accessToken: string; roles: Role[] }
19+
): Promise<boolean> => {
20+
const isAuthorized = await authService.isAuthorizedByRole(accessToken, new Set(roles));
21+
return isAuthorized;
22+
},
23+
isAuthorizedByUserId: async (
24+
_parent: undefined,
25+
{ accessToken, userId }: { accessToken: string; userId: string }
26+
): Promise<boolean> => {
27+
const isAuthorized = await authService.isAuthorizedByUserId(accessToken, userId);
28+
return isAuthorized;
29+
},
30+
isAuthorizedByEmail: async (
31+
_parent: undefined,
32+
{ accessToken, email }: { accessToken: string; email: string }
33+
): Promise<boolean> => {
34+
const isAuthorized = await authService.isAuthorizedByEmail(accessToken, email);
35+
return isAuthorized;
36+
},
37+
},
38+
Mutation: {
39+
login: async (
40+
_parent: undefined,
41+
{ email, password }: { email: string; password: string }
42+
): Promise<AuthDTO> => {
43+
const authDTO = await authService.generateToken(email, password);
44+
return authDTO;
45+
},
46+
loginWithGoogle: async (
47+
_parent: undefined,
48+
{ idToken }: { idToken: string }
49+
): Promise<AuthDTO> => {
50+
const authDTO = await authService.generateTokenOAuth(idToken);
51+
return authDTO;
52+
},
53+
register: async (_parent: undefined, { user }: { user: RegisterUserDTO }): Promise<AuthDTO> => {
54+
if (!user.password) {
55+
throw new Error('Password is required for registration');
56+
}
57+
const newUser = await userService.createUser(
58+
{
59+
...user,
60+
role: Role.User, // Default role for registration
61+
},
62+
undefined,
63+
SignUpMethod.PASSWORD
64+
);
65+
await authService.sendEmailVerificationLink(newUser.email);
66+
const authDTO = await authService.generateToken(user.email, user.password);
67+
return authDTO;
68+
},
69+
refresh: async (
70+
_parent: undefined,
71+
{ refreshToken }: { refreshToken: string }
72+
): Promise<string> => {
73+
const token = await authService.renewToken(refreshToken);
74+
return token.accessToken;
75+
},
76+
logout: async (_parent: undefined, { userId }: { userId: string }): Promise<boolean> => {
77+
await authService.revokeTokens(userId);
78+
return true;
79+
},
80+
resetPassword: async (_parent: undefined, { email }: { email: string }): Promise<boolean> => {
81+
await authService.resetPassword(email);
82+
return true;
83+
},
84+
},
85+
};
86+
87+
export default authResolvers;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import nodemailerConfig from '../../nodemailer.config';
2+
import AuthService from '../../services/implementations/authService';
3+
import EmailService from '../../services/implementations/emailService';
4+
import UserService from '../../services/implementations/userService';
5+
import IAuthService from '../../services/interfaces/authService';
6+
import IEmailService from '../../services/interfaces/emailService';
7+
import IUserService from '../../services/interfaces/userService';
8+
import { CreateUserDTO, UpdateUserDTO, UserDTO } from '../../types';
9+
10+
const userService: IUserService = new UserService();
11+
const emailService: IEmailService = new EmailService(nodemailerConfig);
12+
const authService: IAuthService = new AuthService(userService, emailService);
13+
14+
const userResolvers = {
15+
Query: {
16+
userById: async (_parent: undefined, { id }: { id: string }): Promise<UserDTO> => {
17+
return userService.getUserById(id);
18+
},
19+
userByEmail: async (_parent: undefined, { email }: { email: string }): Promise<UserDTO> => {
20+
return userService.getUserByEmail(email);
21+
},
22+
users: async (): Promise<UserDTO[]> => {
23+
return userService.getUsers();
24+
},
25+
},
26+
Mutation: {
27+
createUser: async (_parent: undefined, { user }: { user: CreateUserDTO }): Promise<UserDTO> => {
28+
const newUser = await userService.createUser(user);
29+
await authService.sendEmailVerificationLink(newUser.email);
30+
return newUser;
31+
},
32+
updateUser: async (
33+
_parent: undefined,
34+
{ id, user }: { id: string; user: UpdateUserDTO }
35+
): Promise<UserDTO> => {
36+
return userService.updateUserById(id, user);
37+
},
38+
deleteUserById: async (_parent: undefined, { id }: { id: string }): Promise<boolean> => {
39+
await userService.deleteUserById(id);
40+
return true;
41+
},
42+
deleteUserByEmail: async (
43+
_parent: undefined,
44+
{ email }: { email: string }
45+
): Promise<boolean> => {
46+
await userService.deleteUserByEmail(email);
47+
return true;
48+
},
49+
},
50+
};
51+
52+
export default userResolvers;

backend/graphql/types/authType.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { gql } from 'apollo-server-express';
2+
3+
const authType = gql`
4+
type AuthDTO {
5+
id: ID!
6+
firstName: String!
7+
lastName: String!
8+
email: String!
9+
role: Role!
10+
accessToken: String!
11+
refreshToken: String!
12+
}
13+
14+
input RegisterUserDTO {
15+
firstName: String!
16+
lastName: String!
17+
email: String!
18+
password: String!
19+
}
20+
21+
extend type Query {
22+
isAuthorizedByRole(accessToken: String!, roles: [Role!]!): Boolean!
23+
isAuthorizedByUserId(accessToken: String!, userId: ID!): Boolean!
24+
isAuthorizedByEmail(accessToken: String!, email: String!): Boolean!
25+
}
26+
27+
extend type Mutation {
28+
login(email: String!, password: String!): AuthDTO!
29+
loginWithGoogle(idToken: String!): AuthDTO!
30+
register(user: RegisterUserDTO!): AuthDTO!
31+
refresh(refreshToken: String!): String!
32+
logout(userId: ID!): Boolean!
33+
resetPassword(email: String!): Boolean!
34+
}
35+
`;
36+
37+
export default authType;

backend/graphql/types/userType.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { gql } from 'apollo-server-express';
2+
3+
const userType = gql`
4+
enum Role {
5+
User
6+
Admin
7+
}
8+
9+
type UserDTO {
10+
id: ID!
11+
firstName: String!
12+
lastName: String!
13+
email: String!
14+
role: Role!
15+
}
16+
17+
input CreateUserDTO {
18+
firstName: String!
19+
lastName: String!
20+
email: String!
21+
role: Role!
22+
password: String
23+
}
24+
25+
input UpdateUserDTO {
26+
firstName: String!
27+
lastName: String!
28+
email: String!
29+
role: Role!
30+
}
31+
32+
extend type Query {
33+
userById(id: ID!): UserDTO!
34+
userByEmail(email: String!): UserDTO!
35+
users: [UserDTO!]!
36+
}
37+
38+
extend type Mutation {
39+
createUser(user: CreateUserDTO!): UserDTO!
40+
updateUser(id: ID!, user: UpdateUserDTO!): UserDTO!
41+
deleteUserById(id: ID!): Boolean!
42+
deleteUserByEmail(email: String!): Boolean!
43+
}
44+
`;
45+
46+
export default userType;

backend/middlewares/auth.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { AuthenticationError, ExpressContext } from 'apollo-server-express';
2+
import { GraphQLResolveInfo } from 'graphql';
3+
import AuthService from '../services/implementations/authService';
4+
import UserService from '../services/implementations/userService';
5+
import IAuthService from '../services/interfaces/authService';
6+
import { Role } from '../types';
7+
8+
const authService: IAuthService = new AuthService(new UserService());
9+
10+
/* eslint-disable @typescript-eslint/no-explicit-any */
11+
export const getAccessToken = (req: any): string | null => {
12+
const authHeaderParts = req.headers.authorization?.split(' ');
13+
if (
14+
authHeaderParts &&
15+
authHeaderParts.length >= 2 &&
16+
authHeaderParts[0].toLowerCase() === 'bearer'
17+
) {
18+
return authHeaderParts[1];
19+
}
20+
return null;
21+
};
22+
23+
/* eslint-disable @typescript-eslint/no-explicit-any */
24+
25+
export const isAuthorizedByRole = (roles: Set<Role>) => {
26+
return async (
27+
resolve: (parent: any, args: any, context: ExpressContext, info: GraphQLResolveInfo) => any,
28+
parent: any,
29+
args: any,
30+
context: ExpressContext,
31+
info: GraphQLResolveInfo
32+
) => {
33+
const accessToken = getAccessToken(context.req);
34+
35+
const authorized = accessToken && (await authService.isAuthorizedByRole(accessToken, roles));
36+
37+
if (!authorized) {
38+
throw new AuthenticationError('You are not authorized to perform this action');
39+
}
40+
41+
return resolve(parent, args, context, info);
42+
};
43+
};
44+
45+
export const isAuthorizedByUserId = () => {
46+
return async (
47+
resolve: (parent: any, args: any, context: ExpressContext, info: GraphQLResolveInfo) => any,
48+
parent: any,
49+
args: { id: string },
50+
context: ExpressContext,
51+
info: GraphQLResolveInfo
52+
) => {
53+
const accessToken = getAccessToken(context.req);
54+
55+
const authorized =
56+
accessToken && (await authService.isAuthorizedByUserId(accessToken, args.id));
57+
58+
if (!authorized) {
59+
throw new AuthenticationError('You are not authorized to perform this action');
60+
}
61+
62+
return resolve(parent, args, context, info);
63+
};
64+
};
65+
66+
export const isAuthorizedByEmail = () => {
67+
return async (
68+
resolve: (parent: any, args: any, context: ExpressContext, info: GraphQLResolveInfo) => any,
69+
parent: any,
70+
args: { email: string },
71+
context: ExpressContext,
72+
info: GraphQLResolveInfo
73+
) => {
74+
const accessToken = getAccessToken(context.req);
75+
76+
const authorized =
77+
accessToken && (await authService.isAuthorizedByEmail(accessToken, args.email));
78+
79+
if (!authorized) {
80+
throw new AuthenticationError('You are not authorized to perform this action');
81+
}
82+
83+
return resolve(parent, args, context, info);
84+
};
85+
};

backend/models/user.model.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Column, DataType, Model, Table } from 'sequelize-typescript';
2+
import { Role } from '../types';
3+
4+
@Table({ tableName: 'users' })
5+
export default class User extends Model {
6+
@Column({ type: DataType.INTEGER, primaryKey: true, autoIncrement: true })
7+
id!: number;
8+
9+
@Column({ type: DataType.STRING, allowNull: false })
10+
firstName!: string;
11+
12+
@Column({ type: DataType.STRING, allowNull: false })
13+
lastName!: string;
14+
15+
@Column({ type: DataType.STRING, allowNull: false, unique: true })
16+
email!: string;
17+
18+
@Column({ type: DataType.STRING, allowNull: false, unique: true })
19+
authId!: string;
20+
21+
@Column({ type: DataType.ENUM('User', 'Admin'), allowNull: false })
22+
role!: Role;
23+
24+
@Column({ type: DataType.DATE, allowNull: false })
25+
createdAt!: Date;
26+
27+
@Column({ type: DataType.DATE, allowNull: false })
28+
updatedAt!: Date;
29+
}

0 commit comments

Comments
 (0)