Authentication with AdminJS For A nestJS Project

Fasai (Prin) Pulkes
5 min readJul 20, 2022

--

Introduction:

I will describe how I added adminJS to a nestJS project (I used typescript, typeORM adapter, express framework). I will also explain how to create a user entity for authentication for the admin panel.

This page assumes you have already created a project and that it is ready to be connected to adminJS.

First, the following modules will need to be installed (from https://docs.adminjs.co/module-@adminjs_nestjs.html).

  • adminJS
npm install adminjs @adminjs/nestjs
  • adminJS express plugin
npm install express @adminjs/express express-formidable
  • express-session
npm install express-session
  • adminJS typeORM adapter
npm install @adminjs/typeorm

Connecting adminJS to your project:

In app.module.ts, register the typeORM adapter and create the admin panel. If using class-validator for input validation, also add the bolded code (this injects it to resource).

Register adapter:

import AdminJS from 'adminjs';
import { Module } from '@nestjs/common';
import { AdminModule } from '@adminjs/nestjs';
import { Database, Resource } from '@adminjs/typeorm'
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { validate } from 'class-validator'
Resource.validate = validate;AdminJS.registerAdapter({ Database, Resource })

In imports, create the admin panel:

@Module({
imports: [
AdminModule.createAdmin({
adminJsOptions: {
rootPath: '/admin',
resources: [
{
resource: User, //replace with name of entity
},
... //Can add more entities using the same format as above
],
}
}),
],
})

Creating a user entity:

Now, we will create a user entity with fields including email/username, password and encryptedPassword. This user will be used to login to the admin panel.

My user entity (called AdminUser):

@ObjectType()
@Entity('admin_user')
export class AdminUser extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
@Field()
id: string;

@Column({ unique: true })
@Field()
email: string;

@Column({ nullable: true })
@HideField()
password: string;

@Column()
@HideField()
encryptedPassword: string;

@Column()
@Field()
role: string;
}

The user ID is auto generated. The email field is set to unique to prevent multiple users using the same email. The password field has nullable set to true as we will encrypt the password before saving it to the database and the password field will be set to null to ensure it is not visible on the database.

For this project, I only allowed querying and mutating user accounts through graphQL (not the adminJS panel) as this allows us to be able to encrypt the password before saving it to the database (adminJS has already programmed these mutations for you, which makes it’s hard to change, but graphQL requires you to program your own queries and mutations). To ensure authentication is required to do these mutations, I used graphQL guards, which will be explained in another post.

Below is an example of how I wrote the functions for querying and mutating (in the admin-user.service.ts):

@Injectable()
export class AdminUserService {
constructor(
@InjectRepository(AdminUser)
private AdminUserRepo: Repository<AdminUser>,
) {}

// If email already exists, raises error
async create(createAdminUserInput: CreateAdminUserInput) {
createAdminUserInput.encryptedPassword = await bcrypt.hash(
createAdminUserInput.password,
10,
);
createAdminUserInput.password = null;
return this.AdminUserRepo.save(createAdminUserInput);
}

async findAll() {
return this.AdminUserRepo.find();
}

async findOne(id: string) {
return this.AdminUserRepo.findOne({
where: {
id,
},
});
}

async findOneEmail(email: string) {
return this.AdminUserRepo.findOne({
email: email,
});
}

async update(updateAdminUserInput: UpdateAdminUserInput) {
updateAdminUserInput.encryptedPassword = await bcrypt.hash(
updateAdminUserInput.password,
10,
);
//Encrypts and stores password
updateAdminUserInput.password = null; //makes password field null so no one can see it
return this.AdminUserRepo.save(updateAdminUserInput);
}

// returns true if successfully deletes user
async remove(id: string) {
const userP = await this.AdminUserRepo.findOne({ id: id });
if (userP) {
this.AdminUserRepo.remove(userP);
return true;
}
return false;
}
}

As seen above, my create and update functions involve encrypting the password using bcrypt before setting the encryptedPassword field to it and setting the password field to null.

This is what my admin-user.resolver.ts file looked like (works the same way as any other entity’s resolver file would):

@Resolver(() => AdminUser)
@UseGuards(JwtAuthGuard)
export class AdminUserResolver {
constructor(private readonly adminUserService: AdminUserService) {}

@Mutation(() => AdminUser)
createAdminUser(
@Args('createAdminUserInput') createAdminUserInput: CreateAdminUserInput,
) {
return this.adminUserService.create(createAdminUserInput);
}

@Query(() => [AdminUser], { name: 'adminUser' })
findAll() {
return this.adminUserService.findAll();
}

@Query(() => AdminUser)
findOne(@Args('id') id: string) {
return this.adminUserService.findOne(id);
}

@Query(() => AdminUser)
findOneEmail(@Args('email') email: string) {
return this.adminUserService.findOneEmail(email);
}

@Mutation(() => AdminUser)
updateAdminUser(
@Args('updateAdminUserInput') updateAdminUserInput: UpdateAdminUserInput,
) {
return this.adminUserService.update(updateAdminUserInput);
}

@Mutation(() => Boolean)
removeAdminUser(@Args('id') id: string) {
return this.adminUserService.remove(id);
}
}

And my admin-user.module.ts file:

@Module({
imports: [TypeOrmModule.forFeature([AdminUser])],
providers: [AdminUserResolver, AdminUserService],
exports: [AdminUserService],
})
export class AdminUserModule {}

Adding adminJS authentication

Now, we can add the authentication for adminJS by customizing the auth option in createAdmin (in the app.module.ts file). Add the bolded code below to add authentication.

@Module({
imports: [
AdminModule.createAdmin({
adminJsOptions: {
rootPath: '/admin',
resources: [
{
resource: User, //replace with name of entity
},
... //Can add more entities using the same format as above
],
},
auth: {
authenticate: async (email, password) => {
const user = await AdminUser.findOne({ email });
if (user) {
const matched = await bcrypt.compare(password, user.encryptedPassword);
if (matched) {
return Promise.resolve({ email: email });
}
}
return null;
},
cookieName: 'admin',
cookiePassword:
'enterString',
},

}),
],
})

I will explain what specific lines of code do:

authenticate: async (email, password) => {

This line of code takes in the typed email and password as input.

const user = await AdminUser.findOne({ email });

This finds a user in the database that has the same email as the one that was input (AdminUser is the name of my user entity for adminJS).

if (user) {
const matched = await bcrypt.compare(password, user.encryptedPassword);

This checks if a user with that email exists in the database. If the user exists, it compares the password entered with the user’s encryptedPassword (encrypted using bcrypt so bcrypt.compare compares an unencrypted password with an encrypted one). The matched field is set to true if the passwords match.

if (matched) {
return Promise.resolve({ email: email });
}

If the passwords match, the user is authenticated and will access the admin panel. If not, the function returns null and a ‘username or password is incorrect’ message will appear.

Customizing how certain fields are shown in the admin panel

Some customizations can be done to the admin panel in adminJS options (in createAdmin in app.module.ts). For instance, if an entity has a field that is a JSON object, the specific types of each subfield can be explicitly specified so they show as separate fields on the admin panel. An example of this is shown below where the field endCustomerProfile is a JSON object:

This project above has an entity called SmsSubscription and this entity is added as a resource. It has a field called endCustomerProfile that is a JSON object. The JSON object contains fields that contain the last name and first name of customers. Thus, in properties, the type of endCustomerProfile (the JSON object) is ‘mixed’ and the two subfields (lastName and firstName) are specified as type ‘string’.

--

--