{"id":14956779,"url":"https://github.com/erickit/nest-user-auth","last_synced_at":"2025-04-07T17:11:04.198Z","repository":{"id":44523860,"uuid":"171186954","full_name":"EricKit/nest-user-auth","owner":"EricKit","description":"A starter build for a back end which implements managing users with MongoDB, Mongoose, NestJS, Passport-JWT, and GraphQL.","archived":false,"fork":false,"pushed_at":"2023-04-11T19:16:14.000Z","size":803,"stargazers_count":294,"open_issues_count":17,"forks_count":40,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-31T14:15:09.656Z","etag":null,"topics":["apollo-server","authentication","backend","graphql","jest","joi","mongo","mongodb","mongoose","nest","nestjs","node","nodejs","nodemailer","passport-jwt","passportjs","supertest","user-management"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EricKit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-02-18T00:02:58.000Z","updated_at":"2025-02-26T07:34:00.000Z","dependencies_parsed_at":"2024-09-02T15:41:33.500Z","dependency_job_id":null,"html_url":"https://github.com/EricKit/nest-user-auth","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricKit%2Fnest-user-auth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricKit%2Fnest-user-auth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricKit%2Fnest-user-auth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EricKit%2Fnest-user-auth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EricKit","download_url":"https://codeload.github.com/EricKit/nest-user-auth/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247694876,"owners_count":20980733,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apollo-server","authentication","backend","graphql","jest","joi","mongo","mongodb","mongoose","nest","nestjs","node","nodejs","nodemailer","passport-jwt","passportjs","supertest","user-management"],"created_at":"2024-09-24T13:13:30.679Z","updated_at":"2025-04-07T17:11:04.177Z","avatar_url":"https://github.com/EricKit.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nest-user-auth\n\n![last commit](https://img.shields.io/github/last-commit/EricKit/nest-user-auth.svg) ![repo size](https://img.shields.io/github/repo-size/EricKit/nest-user-auth.svg) ![open issues](https://img.shields.io/github/issues-raw/EricKit/nest-user-auth.svg) ![liscense](https://img.shields.io/github/license/erickit/nest-user-auth.svg)\n\nIf this project helps you, please add a star! If you see an issue, please post it!\n\nThis project uses NestJS, GraphQL, and MongoDB.\n\nThis project implements user authentication. It will be easy to add other GraphQL schemas following the same structure. User auth is implemented in this project because it is one of the hardest and most common things to create for an API.\n\nThe intent of this project is to provide an example of how to integrate all of these technologies together that are in the NestJS documentation (NestJS, GraphQL, MongoDB, Mongoose, Passport, JWT, DotEnv, Joi, Jest) into a working backend. If you recognize an anti-pattern or a better way to do something, please post an issue.\n\n![preview](assets/preview.png)\n\n## Getting Started\n\nEnsure a MongoDB server is running locally.\n\n### Create a development.env file\n\nAdd a `development.env` file to the root of your project.\n\n```env\nMONGO_URI=mongodb://localhost:27017/user-auth\nJWT_SECRET=someSecret\nEMAIL_ENABLED=true\nEMAIL_SERVICE=Mailgun\nEMAIL_USERNAME=email@mailgun.com\nEMAIL_PASSWORD=emailSMTPpassword\nEMAIL_FROM=from@somedomain.com\n```\n\n#### Required Parameters\n\n`MONGO_URI` the location of your mongo server and database name you want\n\n`JWT_SECRET` a secret string used to make the keys. Create a random string.\n\n#### Optional Parameters\n\n`MONGO_AUTH_ENABLED` set to `true` if your database requires a username and password. If `true`, the user specified by `MONGO_USER` must exist on the database specified in the `MONGO_URI` option. If `true`, `MONGO_USER` and `MONGO_PASSWORD` are required.\n\n`MONGO_USER`, `MONGO_PASSWORD` the user and password for authentication. Recommend a role with `readWrite`.\n\n`JWT_EXPIRES_IN` Seconds until token expires. If not set, there will be no expiration.\n\n`EMAIL_ENABLED` If email services should be used, `EMAIL_*` fields are required if enabled.\n\n`EMAIL_SERVICE` Nodemailer \"Well Known Service\" https://nodemailer.com/smtp/well-known/\n\n`EMAIL_USERNAME`, `EMAIL_PASSWORD` Information for the SMTP service. On Mailgun it is the credentials under Domains -\u003e SMTP Credentials. Use the SMTP service, not the API.\n\n`EMAIL_FROM` The email address the program will use as the from address.\n\n`TEST_EMAIL_TO` When running tests, where emails will be sent. This should be a real email address you own to verify emails are getting out.\n\n### Start the server\n\n`npm install`\n\n`npm run start`\n\nThat's it, the graphQL playground is found at `http://localhost:3000/graphql`\n\n## Model Management\n\nIt is challenging not to repeat the structure of the models in the GraphQL schema, Mongo schema, and Typescript interfaces. The goal is to have one truth point for the models and extend that data when more data is needed.\n\nWith NestJS 6.0.0 a **code first** approach was introduced. This project uses the **schema first** approach to be language agnostic. The starting point for models is the `*.types.graphql` files. They contain the GraphQL schema and have properties that every model, at a minimum, should have.\n\n`@nestjs/graphql` creates a `graphql.classes.ts` file to match the GraphQL schema when the program is started. These classes are used as the base class for the Mongoose Schema and in place of DTOs. Of note, the `IMutation` and `IQuery` classes created by `@nestjs/graphql` are not extended by the resolver class, though it would be nice if they were. It doesn't appear possible without modification of the `grahql.classes.ts` file because all the methods aren't implemented in the same resolver.\n\n`username` is the primary field to identify a user in a request. Initially `username` or `email` were accepted, but for simplicity the schema moved to only username. Both username and email fields are in the JWT data, and because they are both unique, either could be used.\n\nThe database stores a unique lowercase value for both username and email. This is to lookup the user's username or email without case being a factor. Lowercase username and email are also unique, therefore user@Email.com and user@email.com can't both register. The normal cased version is used for everything except lookup. GraphQL Schemas are not aware lowercase values exist intentionally.\n\nThe database handles creating the lowercase values with hooks for `save` and `findOneAndUpdate`. If another method is used to update or save a User, ensure a hook is created to create the lowercase values.\n\n## Users\n\nAdd a user via the graphql playground or a frontend. See example mutations and queries below.\n\nUpdate that user's Document to have the string `admin` in the permissions array. Only an admin can add another admin, so the first user must be done manually. MongoDB Compass is a great tool to modify fields. That user can now add the admin permission or remove the admin permission to or from other users.\n\nThe `UsersService` `update` method will update any fields which are valid and not duplicates, even if other fields are invalid or duplicates.\n\nUsers can change their `username`, `password`, `email`, or `enabled` status via a mutation. Changing their username will make their token unusable (it won't authenticate when the user presenting the token's username is checked against the token's username). This may or may not be the desired behavior. If using on a front end, make it obvious that if the user changes their username, it'll log the user out (or the front end must get a new token via logging in behind the scenes - but this would likely require storing the password and is not recommended).\n\nIf a user sets `enabled` to `false` on their account, they cannot log back in (because it is disabled), only an admin can change it back.\n\nBecause both unique properties `username` and `email` can be changed, `_id` should be used as keys for relationships.\n\nSee `test/users.e2e-spec.ts` for expected results to mutations and queries.\n\n## Environments\n\nAdd a `test.env` file which contains a different `MONGO_URI` than `development.env`. See the testing section for details.\n\nAdd any other environments for production and test. The environment variable `NODE_ENV` is used to determine the correct environment to work in. The program defaults to `development` if there is not a `NODE_ENV` environment variable set. For example, if the configuration is stored in `someEnv.env` file in production then set the `NODE_ENV` environment variable to `someEnv`. This can be done through `package.json` scripts, local environment variables, or your `launch.json` configuration in VS Code. If you do nothing, it will look for `development.env`. Do not commit this file.\n\n## Authentication\n\nAdd the token to your headers `{\"Authorization\": \"Bearer eyj2aGc...\"}` to be authenticated via the `JwtAuthGuard`.\n\nIf a user's account property `enabled` is set to false, their token will no longer authenticate. Many critiques of JWTs vs. session based authentication solutions are that a JWT cannot be invalidated once issued. While that is true, no request will authenticate with a valid JWT while the account associated with the token's `enabled` field is false. An admin or the user can set that field via an update.\n\nAdmin must be set manually as a string in permissions for the first user (add `admin` to the permissions array). That person can then add admin to other users via a mutation. Permissions is an array of strings so that other permissions can be added to allow custom guards.\n\nUsers can modify or view their own data. Admins can do anything except refresh another user's token or change their password, which would allow the admin to impersonate that user.\n\nThe `UsernameEmailGuard` compares the user's email or username with the same field in a query. If any query or mutation in the resolver has `doAnythingWithUser(username: string)` or `doAnythingWithUser(email: string)` and that email / username matches the user which is requesting the action, it will be approved. Username and email are unique, and the user has already been verified via JWT. **If there is not a username or email in the request, it will pass.** This is because the resolvers will set the action on the user making the request. For example, on `updateUser` if no username is specified, the modification is on the user making the request.\n\nThe `UsernameEmailAdminGuard` is the same as the `UsernameEmailGuard` except it also allows admins. Admins should not be allowed to change everything. For example, an admin should not be allowed to set another user's password. This would allow the admin to impersonate that user. The `@AdminAllowedArgs` decorator has been added for this reason to this guard. If this decorator is used, only the arguments specified are allowed. Placing the below decorator above the `updateUser` resolver will not allow an admin to specify the `fieldsToUpdate.password` argument.\n\n```Typescript\n@AdminAllowedArgs(\n    'username',\n    'fieldsToUpdate.username',\n    'fieldsToUpdate.email',\n    'fieldsToUpdate.enabled',\n  )\n```\n\nThe `AdminGuard` only allows admins.\n\nThe `JwtAuthGuard` ensures that there is a valid JWT and that the user associated with the JWT exists in the database.\n\nThe User's Document is accessable in the resolver via `@Context('req')` should it be needed. For example, a user creates a Purchase and that user's ID needs to be attached to the purchase. An example mutation is shown below.\n\n```Typescript\n  // This is an example of how to get access to the validated user making the request\n  @UseGuards(JwtAuthGuard)\n  @Mutation('userInResolver')\n  userInResolver(@Context('req') request: any) {\n    const user: UserDocument = request.user;\n  }\n```\n\n## Relationships\n\nTo add a relationship with the NestJS Schema first approach and Mongoose there are a few caveats. Take for example a one-to-many relationship where a Purchase can be made by one user, but a user can have many purchases. Likely, the Purchase GraphQL schema will look like this:\n\n```graphql\ntype Purchase {\n  product: String!\n  customer: User!\n  ...\n}\n```\n\nThis allows a user to make a query that contains both the purchase and its customer's subfields (see below for security concerns). The Schema first approach will create a file that contains the `Purchase` class, as defined by the schema above, with the `customer` property of type `User`. For the MongoDB Schema and Document, a different field for the foreign key must be created. For example:\n\n```typescript\nexport interface PurchaseDocument extends Purchase, Document {\n// Declaring properties that are not in the GraphQL Schema for a Purchase\n  customerId: Types.ObjectId;\n}\n\nexport const PurchaseDocument: Schema = new Schema(\n  {\n    ...,\n    customerId: {\n      type: Types.ObjectId,\n      ref: 'User',\n    },\n  })\n```\n\nThe `customerId` property of the `PurchaseDocument` interface can reference the `ObjectId` and the `customer` property of the `Purchase` class can reference the `User` class. The `Purchase` class as defined by the schema only has a `customer` property, while the `PurchaseDocument` has both the `customer` and `customerId` properties. This makes sense because a user should never care about how the relationship is built. Below is an example of how the customer's information, including ID, can be queried.\n\n```Typescript\n@ResolveProperty()\nasync customer(@Parent() purchase: PurchaseDocument): Promise\u003cUser\u003e {\n  const userDocument = await this.usersService.findOneById(comment.customerId);\n  return userDocument;\n}\n```\n\n```Graphql\nquery purchase {\n  purchase(id: \"35\") {\n    price\n    customer {\n      username\n    }\n  }\n}\n```\n\nKeep in mind, the above example would create a security issue as every field of a `User` would be accessable to anyone querying a Location. To fix this, add a new type to the GraphQL schema such as `SanitizedUser` which contains only public fields. Then, the `Purchase.customer` property would be changed from `User` to `SanitizedUser`.\n\nIt would be nice to have the `customer` property be a union of a `MongoId` and `User`. This would allow Mongoose's `populate` method to be used to replace the `MongoId` with a `User`. However, a property cannot be made more generic when extending a class.\n\n## Testing\n\nTo test, ensure that the environment is different than the `development` environment. When the end to end tests run, they will delete all users in the database specified in the environment file on start. Currently running `npm run test:e2e` will set `NODE_ENV` to `test` based on `package.json` scripts. This will default to the `test.env` file.\n\nCreate `test.env` to have a different database than the `development.env` file. To test Nodemailer include the variable `TEST_EMAIL_TO` which is the email that will receive the password reset email.\n\n### Example `test.env`\n\n```env\nMONGO_URI=mongodb://localhost:27017/user-auth-test\nJWT_SECRET=someSecret\nEMAIL_SERVICE=Mailgun\nEMAIL_USERNAME=email@mailgun.com\nEMAIL_PASSWORD=emailSMTPpassword\nEMAIL_FROM=from@somedomain.com\nTEST_EMAIL_TO=realEmailAddress@somedomain.com\n```\n\n## nodemon\n\nTo use nodemon there is a small change required. Because the classes file is built from the schema, it is recreated on each launch. This causes nodemon to restart on a loop. Add `src/graphql.classes.ts` to the `ignore` array in `nodemon.json` to ignore the changes to that file.\n\n```typescript\n{\n  \"ignore\": [\"src/**/*.spec.ts\", \"src/graphql.classes.ts\"],\n}\n```\n\n## Next tasks\n\nAdd email verification when a user registers.\n\n## GraphQL Playground Examples\n\n```graphql\nquery loginQuery($loginUser: LoginUserInput!) {\n  login(user: $loginUser) {\n    token\n    user {\n      username\n      email\n    }\n  }\n}\n```\n\n```json\n{\n  \"loginUser\": {\n    \"username\": \"usersname\",\n    \"password\": \"passwordOfUser\"\n  }\n}\n```\n\n```graphql\nquery {\n  users {\n    username\n    email\n  }\n}\n```\n\n```graphql\nquery user {\n  user(email: \"email@test.com\") {\n    username\n  }\n}\n```\n\n```graphql\nquery refreshToken {\n  refreshToken\n}\n```\n\n```graphql\nmutation updateUser($updateUser: UpdateUserInput!) {\n  updateUser(username: \"usernametoUpdate\", fieldsToUpdate: $updateUser) {\n    username\n    email\n    updatedAt\n    createdAt\n  }\n}\n```\n\n```json\n{\n  \"updateUser\": {\n    \"username\": \"newUserName\",\n    \"email\": \"newEmail@test.com\",\n    \"enabled\": false\n  }\n}\n```\n\n```graphql\nmutation CreateUser {\n  createUser(\n    createUserInput: {\n      username: \"username\"\n      email: \"user@test.com\"\n      password: \"userspassword\"\n    }\n  ) {\n    username\n  }\n}\n```\n\n```graphql\nmutation {\n  addAdminPermission(username: \"someUsername\") {\n    permissions\n  }\n}\n```\n\n```graphql\nmutation {\n  removeAdminPermission(username: \"someUsername\") {\n    permissions\n  }\n}\n```\n\n```graphql\nquery {\n  forgotPassword(email: \"some-email@email.com\")\n}\n```\n\n```graphql\nmutation {\n  resetPassword(\n    username: \"username\"\n    code: \"code-from-the-email\"\n    password: \"password\"\n  ) {\n    username\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferickit%2Fnest-user-auth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferickit%2Fnest-user-auth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferickit%2Fnest-user-auth/lists"}