-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add account creation feature and page
Showing
35 changed files
with
2,959 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,4 +14,5 @@ export class HomeComponent { | |
goToLoginPage() { | ||
this.router.navigate(['/login']); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,44 @@ | ||
<div class="login-button"> | ||
<div | ||
id="google-login-button" | ||
class="login-button btn btn-primary" | ||
(click)="signInWithGoogle()" | ||
></div> | ||
<div class="container"> | ||
|
||
<div class="form-container"> | ||
<h2> Create Account</h2> | ||
<form id="userForm" [formGroup]="createAccountForm"> | ||
|
||
<!-- Username --> | ||
<label for="name">Name:</label> | ||
<div *ngIf="createAccountForm.get('username')?.invalid && createAccountForm.get('username')?.touched" | ||
class="error-message"> | ||
<small *ngIf="createAccountForm.get('username')?.hasError('required')">Username is required</small> | ||
</div> | ||
<input type="text" id="name" formControlName="username"> | ||
|
||
|
||
<!-- Email --> | ||
<label for="email">Email:</label> | ||
<div *ngIf="createAccountForm.get('email')?.invalid && createAccountForm.get('email')?.touched" class="error-message"> | ||
<small *ngIf="createAccountForm.get('email')?.hasError('required')">Email is required</small> | ||
<small *ngIf="createAccountForm.get('email')?.hasError('email')">Invalid email address</small> | ||
</div> | ||
|
||
<input type="email" id="email" formControlName="email"> | ||
|
||
<!-- Password --> | ||
<label for="password">Password:</label> | ||
<div *ngIf="createAccountForm.get('password')?.invalid && createAccountForm.get('password')?.touched" | ||
class="error-message"> | ||
<small *ngIf="createAccountForm.get('password')?.hasError('required')">Password is required</small> | ||
<small *ngIf="createAccountForm.get('password')?.hasError('minlength')">Minimum 6 characters</small> | ||
</div> | ||
<input type="password" id="password" formControlName="password"> | ||
|
||
<button class="submit" type="submit" (click)="createAccount()" [disabled]="createAccountForm.invalid">Submit</button> | ||
</form> | ||
|
||
</div> | ||
<!-- <div class="button"> | ||
<div id="google-login-button" class="login-button btn btn-primary" (click)="signInWithGoogle()"></div> | ||
</div> --> | ||
</div> | ||
|
||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
**/node_modules | ||
**/.env | ||
**/.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# CS3219 project: PeerPrep | ||
|
||
## User Service | ||
|
||
### Quick Start | ||
|
||
1. In the `user-service` directory, create a copy of the `.env.sample` file and name it `.env`. | ||
2. Create a MongoDB Atlas Cluster and obtain the connection string. | ||
3. Add the connection string to the `.env` file under the variable `DB_CLOUD_URI`. | ||
4. Ensure you are in the `user-service` directory, then install project dependencies with `npm install`. | ||
5. Start the User Service with `npm start` or `npm run dev`. | ||
6. If the server starts successfully, you will see a "User service server listening on ..." message. | ||
|
||
### Complete User Service Guide: [User Service Guide](./user-service/README.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
DB_CLOUD_URI=<CONNECTION_STRING> | ||
DB_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB | ||
PORT=3001 | ||
|
||
# Will use cloud MongoDB Atlas database | ||
ENV=PROD | ||
|
||
# Secret for creating JWT signature | ||
JWT_SECRET=you-can-replace-this-with-your-own-secret |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Setting up MongoDB Instance for User Service | ||
|
||
1. Visit the MongoDB Atlas Site [https://www.mongodb.com/atlas](https://www.mongodb.com/atlas) and click on "Try Free" | ||
|
||
2. Sign Up/Sign In with your preferred method. | ||
|
||
3. You will be greeted with welcome screens. Feel free to skip them till you reach the Dashboard page. | ||
|
||
4. Create a Database Deployment by clicking on the green `+ Create` Button: | ||
|
||
![alt text](./GuideAssets/Creation.png) | ||
|
||
5. Make selections as followings: | ||
|
||
- Select Shared Cluster | ||
- Select `aws` as Provider | ||
|
||
![alt text](./GuideAssets/Selection1.png) | ||
|
||
- Select `Singapore` for Region | ||
|
||
![alt text](./GuideAssets/Selection2.png) | ||
|
||
- Select `M0 Sandbox` Cluster (Free Forever - No Card Required) | ||
|
||
> Ensure to select M0 Sandbox, else you may be prompted to enter card details and may be charged! | ||
![alt text](./GuideAssets/Selection3.png) | ||
|
||
- Leave `Additional Settings` as it is | ||
|
||
- Provide a suitable name to the Cluster | ||
|
||
![alt text](./GuideAssets/Selection4.png) | ||
|
||
6. You will be prompted to set up Security for the database by providing `Username and Password`. Select that option and enter `Username` and `Password`. Please keep this safe as it will be used in User Service later on. | ||
|
||
![alt text](./GuideAssets/Security.png) | ||
|
||
7. Next, click on `Add my Current IP Address`. This will whiteliste your IP address and allow you to connect to the MongoDB Database. | ||
|
||
![alt text](./GuideAssets/Network.png) | ||
|
||
8. Click `Finish and Close` and the MongoDB Instance should be up and running. | ||
|
||
## Whitelisting All IP's | ||
|
||
1. Select `Network Access` from the left side pane on Dashboard. | ||
|
||
![alt text](./GuideAssets/SidePane.png) | ||
|
||
2. Click on the `Add IP Address` Button | ||
|
||
![alt text](./GuideAssets/AddIPAddress.png) | ||
|
||
3. Select the `ALLOW ACCESS FROM ANYWHERE` Button and Click `Confirm` | ||
|
||
![alt text](./GuideAssets/IPWhitelisting.png) | ||
|
||
Now, any IP Address can access this Database. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
# User Service Guide | ||
|
||
## Setting-up | ||
|
||
> :notebook: If you are familiar to MongoDB and wish to use a local instance, please feel free to do so. This guide utilizes MongoDB Cloud Services. | ||
1. Set up a MongoDB Shared Cluster by following the steps in this [Guide](./MongoDBSetup.md). | ||
|
||
2. After setting up, go to the Database Deployment Page. You would see a list of the Databases you have set up. Select `Connect` on the cluster you just created earlier on for User Service. | ||
|
||
![alt text](./GuideAssets/ConnectCluster.png) | ||
|
||
3. Select the `Drivers` option, as we have to link to a Node.js App (User Service). | ||
|
||
![alt text](./GuideAssets/DriverSelection.png) | ||
|
||
4. Select `Node.js` in the `Driver` pull-down menu, and copy the connection string. | ||
|
||
Notice, you may see `<password>` in this connection string. We will be replacing this with the admin account password that we created earlier on when setting up the Shared Cluster. | ||
|
||
![alt text](./GuideAssets/ConnectionString.png) | ||
|
||
5. In the `user-service` directory, create a copy of the `.env.sample` file and name it `.env`. | ||
|
||
6. Update the `DB_CLOUD_URI` of the `.env` file, and paste the string we copied earlier in step 4. Also remember to replace the `<password>` placeholder with the actual password. | ||
|
||
## Running User Service | ||
|
||
1. Open Command Line/Terminal and navigate into the `user-service` directory. | ||
|
||
2. Run the command: `npm install`. This will install all the necessary dependencies. | ||
|
||
3. Run the command `npm start` to start the User Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. | ||
|
||
4. Using applications like Postman, you can interact with the User Service on port 3001. If you wish to change this, please update the `.env` file. | ||
|
||
## User Service API Guide | ||
|
||
### Create User | ||
|
||
- This endpoint allows adding a new user to the database (i.e., user registration). | ||
|
||
- HTTP Method: `POST` | ||
|
||
- Endpoint: http://localhost:3001/users | ||
|
||
- Body | ||
- Required: `username` (string), `email` (string), `password` (string) | ||
|
||
```json | ||
{ | ||
"username": "SampleUserName", | ||
"email": "sample@gmail.com", | ||
"password": "SecurePassword" | ||
} | ||
``` | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|-------------------------------------------------------| | ||
| 201 (Created) | User created successfully, created user data returned | | ||
| 400 (Bad Request) | Missing fields | | ||
| 409 (Conflict) | Duplicate username or email encountered | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Get User | ||
|
||
- This endpoint allows retrieval of a single user's data from the database using the user's ID. | ||
|
||
> :bulb: The user ID refers to the MongoDB Object ID, a unique identifier automatically generated by MongoDB for each document in a collection. | ||
|
||
- HTTP Method: `GET` | ||
|
||
- Endpoint: http://localhost:3001/users/{userId} | ||
|
||
- Parameters | ||
- Required: `userId` path parameter | ||
- Example: `http://localhost:3001/users/60c72b2f9b1d4c3a2e5f8b4c` | ||
|
||
- <a name="auth-header">Headers</a> | ||
|
||
- Required: `Authorization: Bearer <JWT_ACCESS_TOKEN>` | ||
|
||
- Explanation: This endpoint requires the client to include a JWT (JSON Web Token) in the HTTP request header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. | ||
|
||
- Auth Rules: | ||
|
||
- Admin users: Can retrieve any user's data. The server verifies the user associated with the JWT token is an admin user and allows access to the requested user's data. | ||
|
||
- Non-admin users: Can only retrieve their own data. The server checks if the user ID in the request URL matches the ID of the user associated with the JWT token. If it matches, the server returns the user's own data. | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|----------------------------------------------------------| | ||
| 200 (OK) | Success, user data returned | | ||
| 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | | ||
| 403 (Forbidden) | Access denied for non-admin users accessing others' data | | ||
| 404 (Not Found) | User with the specified ID not found | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Get All Users | ||
|
||
- This endpoint allows retrieval of all users' data from the database. | ||
- HTTP Method: `GET` | ||
- Endpoint: http://localhost:3001/users | ||
- Headers | ||
- Required: `Authorization: Bearer <JWT_ACCESS_TOKEN>` | ||
- Auth Rules: | ||
|
||
- Admin users: Can retrieve all users' data. The server verifies the user associated with the JWT token is an admin user and allows access to all users' data. | ||
|
||
- Non-admin users: Not allowed access. | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|--------------------------------------------------| | ||
| 200 (OK) | Success, all user data returned | | ||
| 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | | ||
| 403 (Forbidden) | Access denied for non-admin users | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Update User | ||
|
||
- This endpoint allows updating a user and their related data in the database using the user's ID. | ||
|
||
- HTTP Method: `PATCH` | ||
|
||
- Endpoint: http://localhost:3001/users/{userId} | ||
|
||
- Parameters | ||
- Required: `userId` path parameter | ||
|
||
- Body | ||
- At least one of the following fields is required: `username` (string), `email` (string), `password` (string) | ||
|
||
```json | ||
{ | ||
"username": "SampleUserName", | ||
"email": "sample@gmail.com", | ||
"password": "SecurePassword" | ||
} | ||
``` | ||
|
||
- Headers | ||
- Required: `Authorization: Bearer <JWT_ACCESS_TOKEN>` | ||
- Auth Rules: | ||
|
||
- Admin users: Can update any user's data. The server verifies the user associated with the JWT token is an admin user and allows the update of requested user's data. | ||
|
||
- Non-admin users: Can only update their own data. The server checks if the user ID in the request URL matches the ID of the user associated with the JWT token. If it matches, the server updates the user's own data. | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|---------------------------------------------------------| | ||
| 200 (OK) | User updated successfully, updated user data returned | | ||
| 400 (Bad Request) | Missing fields | | ||
| 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | | ||
| 403 (Forbidden) | Access denied for non-admin users updating others' data | | ||
| 404 (Not Found) | User with the specified ID not found | | ||
| 409 (Conflict) | Duplicate username or email encountered | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Update User Privilege | ||
|
||
- This endpoint allows updating a user’s privilege, i.e., promoting or demoting them from admin status. | ||
|
||
- HTTP Method: `PATCH` | ||
|
||
- Endpoint: http://localhost:3001/users/{userId} | ||
|
||
- Parameters | ||
- Required: `userId` path parameter | ||
|
||
- Body | ||
- Required: `isAdmin` (boolean) | ||
|
||
```json | ||
{ | ||
"isAdmin": true | ||
} | ||
``` | ||
|
||
- Headers | ||
- Required: `Authorization: Bearer <JWT_ACCESS_TOKEN>` | ||
- Auth Rules: | ||
|
||
- Admin users: Can update any user's privilege. The server verifies the user associated with the JWT token is an admin user and allows the privilege update. | ||
- Non-admin users: Not allowed access. | ||
|
||
> :bulb: You may need to manually assign admin status to the first user by directly editing the database document before using this endpoint. | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|-----------------------------------------------------------------| | ||
| 200 (OK) | User privilege updated successfully, updated user data returned | | ||
| 400 (Bad Request) | Missing fields | | ||
| 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | | ||
| 403 (Forbidden) | Access denied for non-admin users | | ||
| 404 (Not Found) | User with the specified ID not found | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Delete User | ||
|
||
- This endpoint allows deletion of a user and their related data from the database using the user's ID. | ||
- HTTP Method: `DELETE` | ||
- Endpoint: http://localhost:3001/users/{userId} | ||
- Parameters | ||
|
||
- Required: `userId` path parameter | ||
- Headers | ||
|
||
- Required: `Authorization: Bearer <JWT_ACCESS_TOKEN>` | ||
|
||
- Auth Rules: | ||
|
||
- Admin users: Can delete any user's data. The server verifies the user associated with the JWT token is an admin user and allows the deletion of requested user's data. | ||
|
||
- Non-admin users: Can only delete their own data. The server checks if the user ID in the request URL matches the ID of the user associated with the JWT token. If it matches, the server deletes the user's own data. | ||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|---------------------------------------------------------| | ||
| 200 (OK) | User deleted successfully | | ||
| 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | | ||
| 403 (Forbidden) | Access denied for non-admin users deleting others' data | | ||
| 404 (Not Found) | User with the specified ID not found | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Login | ||
|
||
- This endpoint allows a user to authenticate with an email and password and returns a JWT access token. The token is valid for 1 day and can be used subsequently to access protected resources. For example usage, refer to the [Authorization header section in the Get User endpoint](#auth-header). | ||
- HTTP Method: `POST` | ||
- Endpoint: http://localhost:3001/auth/login | ||
- Body | ||
- Required: `email` (string), `password` (string) | ||
|
||
```json | ||
{ | ||
"email": "sample@gmail.com", | ||
"password": "SecurePassword" | ||
} | ||
``` | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|----------------------------------------------------| | ||
| 200 (OK) | Login successful, JWT token and user data returned | | ||
| 400 (Bad Request) | Missing fields | | ||
| 401 (Unauthorized) | Incorrect email or password | | ||
| 500 (Internal Server Error) | Database or server error | | ||
|
||
### Verify Token | ||
|
||
- This endpoint allows one to verify a JWT access token to authenticate and retrieve the user's data associated with the token. | ||
- HTTP Method: `GET` | ||
- Endpoint: http://localhost:3001/auth/verify-token | ||
- Headers | ||
- Required: `Authorization: Bearer <JWT_ACCESS_TOKEN>` | ||
|
||
- Responses: | ||
|
||
| Response Code | Explanation | | ||
|-----------------------------|----------------------------------------------------| | ||
| 200 (OK) | Token verified, authenticated user's data returned | | ||
| 401 (Unauthorized) | Missing/invalid/expired JWT | | ||
| 500 (Internal Server Error) | Database or server error | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import bcrypt from "bcrypt"; | ||
import jwt from "jsonwebtoken"; | ||
import { findUserByEmail as _findUserByEmail } from "../model/repository.js"; | ||
import { formatUserResponse } from "./user-controller.js"; | ||
|
||
export async function handleLogin(req, res) { | ||
const { email, password } = req.body; | ||
if (email && password) { | ||
try { | ||
const user = await _findUserByEmail(email); | ||
if (!user) { | ||
return res.status(401).json({ message: "Wrong email and/or password" }); | ||
} | ||
|
||
const match = await bcrypt.compare(password, user.password); | ||
if (!match) { | ||
return res.status(401).json({ message: "Wrong email and/or password" }); | ||
} | ||
|
||
const accessToken = jwt.sign({ | ||
id: user.id, | ||
}, process.env.JWT_SECRET, { | ||
expiresIn: "1d", | ||
}); | ||
return res.status(200).json({ message: "User logged in", data: { accessToken, ...formatUserResponse(user) } }); | ||
} catch (err) { | ||
return res.status(500).json({ message: err.message }); | ||
} | ||
} else { | ||
return res.status(400).json({ message: "Missing email and/or password" }); | ||
} | ||
} | ||
|
||
export async function handleVerifyToken(req, res) { | ||
try { | ||
const verifiedUser = req.user; | ||
return res.status(200).json({ message: "Token verified", data: verifiedUser }); | ||
} catch (err) { | ||
return res.status(500).json({ message: err.message }); | ||
} | ||
} |
167 changes: 167 additions & 0 deletions
167
peer-prep-user/user-service/controller/user-controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import bcrypt from "bcrypt"; | ||
import { isValidObjectId } from "mongoose"; | ||
import { | ||
createUser as _createUser, | ||
deleteUserById as _deleteUserById, | ||
findAllUsers as _findAllUsers, | ||
findUserByEmail as _findUserByEmail, | ||
findUserById as _findUserById, | ||
findUserByUsername as _findUserByUsername, | ||
findUserByUsernameOrEmail as _findUserByUsernameOrEmail, | ||
updateUserById as _updateUserById, | ||
updateUserPrivilegeById as _updateUserPrivilegeById, | ||
} from "../model/repository.js"; | ||
|
||
export async function createUser(req, res) { | ||
try { | ||
const { username, email, password } = req.body; | ||
if (username && email && password) { | ||
const existingUser = await _findUserByUsernameOrEmail(username, email); | ||
if (existingUser) { | ||
return res.status(409).json({ message: "username or email already exists" }); | ||
} | ||
|
||
const salt = bcrypt.genSaltSync(10); | ||
const hashedPassword = bcrypt.hashSync(password, salt); | ||
const createdUser = await _createUser(username, email, hashedPassword); | ||
return res.status(201).json({ | ||
message: `Created new user ${username} successfully`, | ||
data: formatUserResponse(createdUser), | ||
}); | ||
} else { | ||
return res.status(400).json({ message: "username and/or email and/or password are missing" }); | ||
} | ||
} catch (err) { | ||
console.error(err); | ||
return res.status(500).json({ message: "Unknown error when creating new user!" }); | ||
} | ||
} | ||
|
||
export async function getUser(req, res) { | ||
try { | ||
const userId = req.params.id; | ||
if (!isValidObjectId(userId)) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
|
||
const user = await _findUserById(userId); | ||
if (!user) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} else { | ||
return res.status(200).json({ message: `Found user`, data: formatUserResponse(user) }); | ||
} | ||
} catch (err) { | ||
console.error(err); | ||
return res.status(500).json({ message: "Unknown error when getting user!" }); | ||
} | ||
} | ||
|
||
export async function getAllUsers(req, res) { | ||
try { | ||
const users = await _findAllUsers(); | ||
|
||
return res.status(200).json({ message: `Found users`, data: users.map(formatUserResponse) }); | ||
} catch (err) { | ||
console.error(err); | ||
return res.status(500).json({ message: "Unknown error when getting all users!" }); | ||
} | ||
} | ||
|
||
export async function updateUser(req, res) { | ||
try { | ||
const { username, email, password } = req.body; | ||
if (username || email || password) { | ||
const userId = req.params.id; | ||
if (!isValidObjectId(userId)) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
const user = await _findUserById(userId); | ||
if (!user) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
if (username || email) { | ||
let existingUser = await _findUserByUsername(username); | ||
if (existingUser && existingUser.id !== userId) { | ||
return res.status(409).json({ message: "username already exists" }); | ||
} | ||
existingUser = await _findUserByEmail(email); | ||
if (existingUser && existingUser.id !== userId) { | ||
return res.status(409).json({ message: "email already exists" }); | ||
} | ||
} | ||
|
||
let hashedPassword; | ||
if (password) { | ||
const salt = bcrypt.genSaltSync(10); | ||
hashedPassword = bcrypt.hashSync(password, salt); | ||
} | ||
const updatedUser = await _updateUserById(userId, username, email, hashedPassword); | ||
return res.status(200).json({ | ||
message: `Updated data for user ${userId}`, | ||
data: formatUserResponse(updatedUser), | ||
}); | ||
} else { | ||
return res.status(400).json({ message: "No field to update: username and email and password are all missing!" }); | ||
} | ||
} catch (err) { | ||
console.error(err); | ||
return res.status(500).json({ message: "Unknown error when updating user!" }); | ||
} | ||
} | ||
|
||
export async function updateUserPrivilege(req, res) { | ||
try { | ||
const { isAdmin } = req.body; | ||
|
||
if (isAdmin !== undefined) { // isAdmin can have boolean value true or false | ||
const userId = req.params.id; | ||
if (!isValidObjectId(userId)) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
const user = await _findUserById(userId); | ||
if (!user) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
|
||
const updatedUser = await _updateUserPrivilegeById(userId, isAdmin === true); | ||
return res.status(200).json({ | ||
message: `Updated privilege for user ${userId}`, | ||
data: formatUserResponse(updatedUser), | ||
}); | ||
} else { | ||
return res.status(400).json({ message: "isAdmin is missing!" }); | ||
} | ||
} catch (err) { | ||
console.error(err); | ||
return res.status(500).json({ message: "Unknown error when updating user privilege!" }); | ||
} | ||
} | ||
|
||
export async function deleteUser(req, res) { | ||
try { | ||
const userId = req.params.id; | ||
if (!isValidObjectId(userId)) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
const user = await _findUserById(userId); | ||
if (!user) { | ||
return res.status(404).json({ message: `User ${userId} not found` }); | ||
} | ||
|
||
await _deleteUserById(userId); | ||
return res.status(200).json({ message: `Deleted user ${userId} successfully` }); | ||
} catch (err) { | ||
console.error(err); | ||
return res.status(500).json({ message: "Unknown error when deleting user!" }); | ||
} | ||
} | ||
|
||
export function formatUserResponse(user) { | ||
return { | ||
id: user.id, | ||
username: user.username, | ||
email: user.email, | ||
isAdmin: user.isAdmin, | ||
createdAt: user.createdAt, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import express from "express"; | ||
import cors from "cors"; | ||
|
||
import userRoutes from "./routes/user-routes.js"; | ||
import authRoutes from "./routes/auth-routes.js"; | ||
|
||
const app = express(); | ||
|
||
app.use(express.urlencoded({ extended: true })); | ||
app.use(express.json()); | ||
app.use(cors()); // config cors so that front-end can use | ||
app.options("*", cors()); | ||
|
||
// To handle CORS Errors | ||
app.use((req, res, next) => { | ||
res.header("Access-Control-Allow-Origin", "*"); // "*" -> Allow all links to access | ||
|
||
res.header( | ||
"Access-Control-Allow-Headers", | ||
"Origin, X-Requested-With, Content-Type, Accept, Authorization", | ||
); | ||
|
||
// Browsers usually send this before PUT or POST Requests | ||
if (req.method === "OPTIONS") { | ||
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH"); | ||
return res.status(200).json({}); | ||
} | ||
|
||
// Continue Route Processing | ||
next(); | ||
}); | ||
|
||
app.use("/users", userRoutes); | ||
app.use("/auth", authRoutes); | ||
|
||
app.get("/", (req, res, next) => { | ||
console.log("Sending Greetings!"); | ||
res.json({ | ||
message: "Hello World from user-service", | ||
}); | ||
}); | ||
|
||
// Handle When No Route Match Is Found | ||
app.use((req, res, next) => { | ||
const error = new Error("Route Not Found"); | ||
error.status = 404; | ||
next(error); | ||
}); | ||
|
||
app.use((error, req, res, next) => { | ||
res.status(error.status || 500); | ||
res.json({ | ||
error: { | ||
message: error.message, | ||
}, | ||
}); | ||
}); | ||
|
||
|
||
export default app; |
48 changes: 48 additions & 0 deletions
48
peer-prep-user/user-service/middleware/basic-access-control.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import jwt from "jsonwebtoken"; | ||
import { findUserById as _findUserById } from "../model/repository.js"; | ||
|
||
export function verifyAccessToken(req, res, next) { | ||
const authHeader = req.headers["authorization"]; | ||
if (!authHeader) { | ||
return res.status(401).json({ message: "Authentication failed" }); | ||
} | ||
|
||
// request auth header: `Authorization: Bearer + <access_token>` | ||
const token = authHeader.split(" ")[1]; | ||
jwt.verify(token, process.env.JWT_SECRET, async (err, user) => { | ||
if (err) { | ||
return res.status(401).json({ message: "Authentication failed" }); | ||
} | ||
|
||
// load latest user info from DB | ||
const dbUser = await _findUserById(user.id); | ||
if (!dbUser) { | ||
return res.status(401).json({ message: "Authentication failed" }); | ||
} | ||
|
||
req.user = { id: dbUser.id, username: dbUser.username, email: dbUser.email, isAdmin: dbUser.isAdmin }; | ||
next(); | ||
}); | ||
} | ||
|
||
export function verifyIsAdmin(req, res, next) { | ||
if (req.user.isAdmin) { | ||
next(); | ||
} else { | ||
return res.status(403).json({ message: "Not authorized to access this resource" }); | ||
} | ||
} | ||
|
||
export function verifyIsOwnerOrAdmin(req, res, next) { | ||
if (req.user.isAdmin) { | ||
return next(); | ||
} | ||
|
||
const userIdFromReqParams = req.params.id; | ||
const userIdFromToken = req.user.id; | ||
if (userIdFromReqParams === userIdFromToken) { | ||
return next(); | ||
} | ||
|
||
return res.status(403).json({ message: "Not authorized to access this resource" }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import UserModel from "./user-model.js"; | ||
import "dotenv/config"; | ||
import { connect } from "mongoose"; | ||
|
||
export async function connectToDB() { | ||
let mongoDBUri = | ||
process.env.ENV === "PROD" | ||
? process.env.DB_CLOUD_URI | ||
: process.env.DB_LOCAL_URI; | ||
|
||
await connect(mongoDBUri); | ||
} | ||
|
||
export async function createUser(username, email, password) { | ||
return new UserModel({ username, email, password }).save(); | ||
} | ||
|
||
export async function findUserByEmail(email) { | ||
return UserModel.findOne({ email }); | ||
} | ||
|
||
export async function findUserById(userId) { | ||
return UserModel.findById(userId); | ||
} | ||
|
||
export async function findUserByUsername(username) { | ||
return UserModel.findOne({ username }); | ||
} | ||
|
||
export async function findUserByUsernameOrEmail(username, email) { | ||
return UserModel.findOne({ | ||
$or: [ | ||
{ username }, | ||
{ email }, | ||
], | ||
}); | ||
} | ||
|
||
export async function findAllUsers() { | ||
return UserModel.find(); | ||
} | ||
|
||
export async function updateUserById(userId, username, email, password) { | ||
return UserModel.findByIdAndUpdate( | ||
userId, | ||
{ | ||
$set: { | ||
username, | ||
email, | ||
password, | ||
}, | ||
}, | ||
{ new: true }, // return the updated user | ||
); | ||
} | ||
|
||
export async function updateUserPrivilegeById(userId, isAdmin) { | ||
return UserModel.findByIdAndUpdate( | ||
userId, | ||
{ | ||
$set: { | ||
isAdmin, | ||
}, | ||
}, | ||
{ new: true }, // return the updated user | ||
); | ||
} | ||
|
||
export async function deleteUserById(userId) { | ||
return UserModel.findByIdAndDelete(userId); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import mongoose from "mongoose"; | ||
|
||
const Schema = mongoose.Schema; | ||
|
||
const UserModelSchema = new Schema({ | ||
username: { | ||
type: String, | ||
required: true, | ||
unique: true, | ||
}, | ||
email: { | ||
type: String, | ||
required: true, | ||
unique: true, | ||
}, | ||
password: { | ||
type: String, | ||
required: true, | ||
}, | ||
createdAt: { | ||
type: Date, | ||
default: Date.now, // Setting default to the current date/time | ||
}, | ||
isAdmin: { | ||
type: Boolean, | ||
required: true, | ||
default: false, | ||
}, | ||
}); | ||
|
||
export default mongoose.model("UserModel", UserModelSchema); |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "user-service", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "nodemon server.js", | ||
"start": "node server.js", | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"nodemon": "^3.1.4" | ||
}, | ||
"dependencies": { | ||
"bcrypt": "^5.1.1", | ||
"cors": "^2.8.5", | ||
"dotenv": "^16.4.5", | ||
"express": "^4.19.2", | ||
"jsonwebtoken": "^9.0.2", | ||
"mongodb": "^6.9.0", | ||
"mongoose": "^8.5.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import express from "express"; | ||
|
||
import { handleLogin, handleVerifyToken } from "../controller/auth-controller.js"; | ||
import { verifyAccessToken } from "../middleware/basic-access-control.js"; | ||
|
||
const router = express.Router(); | ||
|
||
router.post("/login", handleLogin); | ||
|
||
router.get("/verify-token", verifyAccessToken, handleVerifyToken); | ||
|
||
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import express from "express"; | ||
|
||
import { | ||
createUser, | ||
deleteUser, | ||
getAllUsers, | ||
getUser, | ||
updateUser, | ||
updateUserPrivilege, | ||
} from "../controller/user-controller.js"; | ||
import { verifyAccessToken, verifyIsAdmin, verifyIsOwnerOrAdmin } from "../middleware/basic-access-control.js"; | ||
|
||
const router = express.Router(); | ||
|
||
router.get("/", verifyAccessToken, verifyIsAdmin, getAllUsers); | ||
|
||
router.patch("/:id/privilege", verifyAccessToken, verifyIsAdmin, updateUserPrivilege); | ||
|
||
router.post("/", createUser); | ||
|
||
router.get("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, getUser); | ||
|
||
router.patch("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, updateUser); | ||
|
||
router.delete("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, deleteUser); | ||
|
||
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import http from "http"; | ||
import index from "./index.js"; | ||
import "dotenv/config"; | ||
import { connectToDB } from "./model/repository.js"; | ||
|
||
const port = process.env.PORT || 3001; | ||
|
||
const server = http.createServer(index); | ||
|
||
await connectToDB().then(() => { | ||
console.log("MongoDB Connected!"); | ||
|
||
server.listen(port); | ||
console.log("User service server listening on http://localhost:" + port); | ||
}).catch((err) => { | ||
console.error("Failed to connect to DB"); | ||
console.error(err); | ||
}); | ||
|