Reputation: 564
I follwed Loopback 4 tutorial to secure Api with @loopback/authentication, @loopback/authentication-jwt. I created a entity todo to follow tutorial example, when I migrate db only todo table is created but User, UserCredential related Tables can't be migrated automatically, official example is given on github it's working fine but it uses in memory db with file.
Is there any way to create tables by migrating using @loopback/authentication-jwt/User entity?
Upvotes: 1
Views: 613
Reputation: 21
I got stuck on the same issue for quite a while but eventually figured out how to do it for MySQL. The gist of it is that you need to create your own models and then override the service and models for User and UserCredentials that are built into the @loopback/authentication-jwt extension. This is actually explained on the github here
In my case what made this non-trivial was that I needed to figure out how to translate the models defined in the extension to data models that you can actually store in MySQL. Once you do so they will migrate correctly if they are in the models folder.
Here are some examples of what I did to get this working:
In the constructor of application.ts
// Mount authentication system
this.component(AuthenticationComponent);
// Mount jwt component
this.component(JWTAuthenticationComponent);
// Bind datasource
this.dataSource(NihongoWordListDataSource, UserServiceBindings.DATASOURCE_NAME);
// Bind user service
this.bind(UserServiceBindings.USER_SERVICE).toClass(AppUserService),
// Bind user and credentials repository
this.bind(UserServiceBindings.USER_REPOSITORY).toClass(
AppUserRepository,
),
this.bind(UserServiceBindings.USER_CREDENTIALS_REPOSITORY).toClass(
AppUserCredentialsRepository,
),
Custom User Service (basically directly copied from the github page only mine is called AppUser):
import {UserService} from '@loopback/authentication';
import {repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {securityId, UserProfile} from '@loopback/security';
import {compare} from 'bcryptjs';
// User --> MyUser
import {AppUser} from '../models';
// UserRepository --> MyUserRepository
import { AppUserRepository } from '../repositories';
export type Credentials = {
email: string;
password: string;
};
// User --> MyUser
export class AppUserService implements UserService<AppUser, Credentials> {
constructor(
// UserRepository --> MyUserRepository
@repository(AppUserRepository) public userRepository: AppUserRepository,
) {}
// User --> MyUser
async verifyCredentials(credentials: Credentials): Promise<AppUser> {
const invalidCredentialsError = 'Invalid email or password.';
const foundUser = await this.userRepository.findOne({
where: {email: credentials.email},
});
if (!foundUser) {
throw new HttpErrors.Unauthorized(invalidCredentialsError);
}
const credentialsFound = await this.userRepository.findCredentials(
foundUser.id,
);
if (!credentialsFound) {
throw new HttpErrors.Unauthorized(invalidCredentialsError);
}
const passwordMatched = await compare(
credentials.password,
credentialsFound.password,
);
if (!passwordMatched) {
throw new HttpErrors.Unauthorized(invalidCredentialsError);
}
return foundUser;
}
// User --> MyUser
convertToUserProfile(user: AppUser): UserProfile {
return {
[securityId]: user.id.toString(),
name: user.username,
id: user.id,
email: user.email,
};
}
}
AppUser Model:
import {Entity, model, property, hasOne} from '@loopback/repository';
import {AppUserCredentials} from './app-user-credentials.model';
@model({
settings: {
idInjection: false,
mysql: {schema: 'nihongo_word_list', table: 'app_user'}
}
})
export class AppUser extends Entity {
@property({
type: 'string',
id: true,
generated: false,
defaultFn: 'uuidv4',
mysql: {columnName: 'id', dataType: 'varchar', dataLength: 36, nullable: 'N'},
})
id: string;
@property({
type: 'string',
mysql: {columnName: 'realm', dataType: 'varchar', dataLength: 36, nullable: 'Y'},
})
realm?: string;
@property({
type: 'string',
mysql: {columnName: 'username', dataType: 'varchar', dataLength: 256, nullable: 'Y'},
})
username?: string;
@property({
type: 'string',
required: true,
mysql: {columnName: 'email', dataType: 'varchar', dataLength: 256, nullable: 'N'},
})
email: string;
@property({
type: 'boolean',
required: true,
mysql: {columnName: 'emailVerified', dataType: 'tinyint', dataLength: 1, nullable: 'Y', default: 0},
})
emailVerified?: boolean;
@property({
type: 'string',
mysql: {columnName: 'verificationToken', dataType: 'varchar', dataLength: 256, nullable: 'N'},
})
verificationToken?: string;
@hasOne(() => AppUserCredentials, {keyTo: 'userId'})
userCredentials: AppUserCredentials;
// Define well-known properties here
// Indexer property to allow additional data
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[prop: string]: any;
constructor(data?: Partial<AppUser>) {
super(data);
}
}
export interface AppUserRelations {
// describe navigational properties here
}
export type AppUserWithRelations = AppUser & AppUserRelations;
You need to do the same for AppUserCredentials.
You also need to set up the repository for your AppUser and AppUserCredentials models.
I hope this puts you on the right track. Note that you can and should use the following npm CLI methods while you're making changes to the migration. I just did this:
// clean your dist directory otherwise built stuff gets left behind and can prevent your app from starting
npm run clean
// rebuild your code into dist folder
npm run build
// migrate your db
npm run migrate
Good luck!
Upvotes: 2