seeders, migrations & roles

This commit is contained in:
Sergey Yarkov 2021-07-23 20:23:34 +03:00
parent bb12cc9bd4
commit 05fc06e744
33 changed files with 933 additions and 64 deletions

View File

@ -20,7 +20,9 @@
"providers": [
"./providers/AppProvider",
"@adonisjs/core",
"@adonisjs/lucid"
"@adonisjs/lucid",
"@adonisjs/auth",
"@adonisjs/redis"
],
"aceProviders": [
"@adonisjs/repl"

View File

@ -8,3 +8,7 @@ PG_PORT=5432
PG_USER=lucid
PG_PASSWORD=
PG_DB_NAME=lucid
REDIS_CONNECTION=local
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

View File

@ -4,5 +4,7 @@
"tabWidth": 2,
"semi": true,
"jsxSingleQuote": true,
"arrowParens": "avoid"
"arrowParens": "avoid",
"endOfLine": "auto",
"printWidth": 100
}

View File

@ -0,0 +1,55 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Logger from '@ioc:Adonis/Core/Logger';
import Hash from '@ioc:Adonis/Core/Hash';
import User from 'App/Models/User';
import { Exception } from '@adonisjs/core/build/standalone';
export default class AuthController {
private user: typeof User;
private guard: 'api';
constructor() {
this.user = User;
}
/**
* Creates a new token for user and returns it.
* POST /login
*
* @param {HttpContextContract} context
* @returns {object} - Token data
*/
public async login({ auth, request }: HttpContextContract) {
const login = request.input('login');
const password = request.input('password');
const user = await this.user.query().preload('roles').where('login', login).firstOrFail();
if (!(await Hash.verify(user.password, password))) {
throw new Exception('Invalid credentials.', 401, 'E_INVALID_CREDENTIALS');
}
const token = await auth.use(this.guard).generate(user, {
expiresIn: '1d',
userRoles: user.roles.map(role => role.slug),
});
Logger.info(`A token for user with id: "${token.user.id}" was successfully generated.`);
return token.toJSON();
}
/**
* Revoke a token if user logged in.
* POST /logout
*
* @param {HttpContextContract} context
* @returns {object} - Revoked message
*/
public async logout({ auth }: HttpContextContract) {
await auth.use(this.guard).revoke();
return { message: 'REVOKED' };
}
}

View File

@ -0,0 +1,4 @@
// import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class ContactsController {
}

View File

@ -0,0 +1,83 @@
import { Exception } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Role from 'App/Models/Role';
import User from 'App/Models/User';
import AddRoleToUserValidator from 'App/Validators/AddRoleToUserValidator';
export default class RolesController {
private readonly user: typeof User;
private readonly role: typeof Role;
constructor() {
this.user = User;
this.role = Role;
}
private async findRolesBySlug(input: any) {
const roles = await this.role.query().whereIn('slug', input);
if (roles.length !== input.length) {
throw new Exception(
'Unable to find any role using input value "roles"',
404,
'E_ROLE_NOT_FOUND'
);
}
return roles;
}
/**
* Add a role to a user by "id"
* POST /user/:id/roles
*
* @param {HttpContextContract} context
* @returns {Promise<User>} - Updated user.
*/
public async store({ request, params }: HttpContextContract): Promise<Role[]> {
try {
await request.validate(AddRoleToUserValidator);
const input = request.input('roles');
const user = await this.user.findOrFail(params.id);
const roles = await this.findRolesBySlug(input);
await user.related('roles').attach(roles.map(r => r.id));
await user.load('roles');
return user.roles;
} catch (error) {
if (error.code === '23505') {
throw new Exception('Some role already attached to that user.', 400, 'E_ROLE_ATTACHED');
}
throw error;
}
}
/**
* Detach role from user by "id"
* DELETE /users/:id/roles
*
*/
public async destroy({ request, params }: HttpContextContract): Promise<Role[]> {
const input = request.input('roles');
const user = await this.user.query().preload('roles').where('id', params.id).firstOrFail();
/**
* Detach all when "roles" input not provided
*/
if (input === undefined) {
user.related('roles').detach([]);
return [];
}
const roles = await this.findRolesBySlug(input);
user.related('roles').detach(roles.map(r => r.id));
await user.load('roles');
return user.roles;
}
}

View File

@ -1,13 +1,21 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Role from 'App/Models/Role';
import User from 'App/Models/User';
import Contact from 'App/Models/Contact';
import CreateUserValidator from 'App/Validators/CreateUserValidator';
import UpdateUserValidator from 'App/Validators/UpdateUserValidator';
export default class UsersController {
private readonly userModel: typeof User;
private readonly user: typeof User;
private readonly role: typeof Role;
private readonly contact: typeof Contact;
constructor() {
this.userModel = User;
this.user = User;
this.role = Role;
this.contact = Contact;
}
/**
@ -18,7 +26,7 @@ export default class UsersController {
*/
public async index(): Promise<User[]> {
const users = this.userModel.all();
const users = await this.user.query().preload('contacts').preload('roles');
return users;
}
@ -31,7 +39,11 @@ export default class UsersController {
*/
public async show({ params }: HttpContextContract): Promise<User> {
const user = await this.userModel.findOrFail(params.user_id);
const user = await this.user.findOrFail(params.id);
await user.load('contacts');
await user.load('roles');
return user;
}
@ -45,21 +57,27 @@ export default class UsersController {
public async store({ request }: HttpContextContract): Promise<User> {
await request.validate(CreateUserValidator);
const user = await this.userModel.create({
name: request.input('name'),
surname: request.input('surname'),
patronymic: request.input('patronymic'),
const roles = await this.role.query().whereIn('slug', request.input('roles'));
const user = await this.user.create({
first_name: request.input('first_name'),
last_name: request.input('last_name'),
login: request.input('login'),
email: request.input('email'),
password: request.input('password'),
});
await user.related('contacts').create({ email: request.input('email') });
await user.related('roles').attach(roles.map(r => r.id));
await user.load('roles');
await user.load('contacts');
return user;
}
/**
* Update a user in a system by "id".
* PUT /users
* PATCH /users/:id
*
* @param {HttpContextContract} context
* @returns {Promise<User>}
@ -67,14 +85,35 @@ export default class UsersController {
public async update({ request, params }: HttpContextContract): Promise<User> {
await request.validate(UpdateUserValidator);
const user = await this.userModel.findOrFail(params.id);
user.name = request.input('name');
user.surname = request.input('surname');
user.patronymic = request.input('patronymic');
/**
* User update
*/
const user = await this.user.findOrFail(params.id);
user.first_name = request.input('first_name');
user.last_name = request.input('last_name');
user.login = request.input('login');
user.email = request.input('email');
user.password = request.input('password');
// await user.load('roles');
/**
* Contacts update
*/
const contacts = await this.contact.findByOrFail('user_id', params.id);
contacts.email = request.input('email');
/**
* Save the updated data
*/
await user.save();
await contacts.save();
/**
* Load the updated data to return
*/
await user.load('roles');
await user.load('contacts');
return user;
}
@ -88,7 +127,7 @@ export default class UsersController {
*/
public async destroy({ params }: HttpContextContract): Promise<User> {
const user = await this.userModel.findOrFail(params.id);
const user = await this.user.findOrFail(params.id);
await user.delete();
return user;

68
app/Middleware/Auth.ts Normal file
View File

@ -0,0 +1,68 @@
import { GuardsList } from '@ioc:Adonis/Addons/Auth';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { AuthenticationException } from '@adonisjs/auth/build/standalone';
/**
* Auth middleware is meant to restrict un-authenticated access to a given route
* or a group of routes.
*
* You must register this middleware inside `start/kernel.ts` file under the list
* of named middleware.
*/
export default class AuthMiddleware {
/**
* The URL to redirect to when request is Unauthorized
*/
protected redirectTo = '/login';
/**
* Authenticates the current HTTP request against a custom set of defined
* guards.
*
* The authentication loop stops as soon as the user is authenticated using any
* of the mentioned guards and that guard will be used by the rest of the code
* during the current request.
*/
protected async authenticate(auth: HttpContextContract['auth'], guards: (keyof GuardsList)[]) {
/**
* Hold reference to the guard last attempted within the for loop. We pass
* the reference of the guard to the "AuthenticationException", so that
* it can decide the correct response behavior based upon the guard
* driver
*/
let guardLastAttempted: string | undefined;
const authPromises: Promise<boolean>[] = guards.map(guard => auth.use(guard).check());
const authResults: boolean[] = await Promise.all(authPromises);
for (let i = 0; i < authResults.length; i += 1) {
/**
* User has been successfully authenticated
*/
if (authResults[i]) return;
}
/**
* Unable to authenticate using any guard
*/
throw new AuthenticationException(
'Unauthorized access',
'E_UNAUTHORIZED_ACCESS',
guardLastAttempted,
this.redirectTo
);
}
public async handle(
{ auth }: HttpContextContract,
next: () => Promise<void>,
customGuards: (keyof GuardsList)[]
) {
/**
* Uses the user defined guards or the default guard mentioned in
* the config file
*/
const guards = customGuards.length ? customGuards : [auth.name];
await this.authenticate(auth, guards);
await next();
}
}

40
app/Middleware/Role.ts Normal file
View File

@ -0,0 +1,40 @@
import { Exception } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class Role {
error: {
message: string;
status: number;
errorCode: string;
};
constructor() {
this.error = {
message: 'You dont have permission to perform that action.',
status: 403,
errorCode: 'E_ACCESS_DENIED',
};
}
/**
* Checks an array of roles for the current user
* If the user role is not found in the array of roles for the route, an error is thrown
*
* @param {string[]} roles - Array of roles for route
* @param {string[]} userRole - Array of roles of current user
*/
protected checkRoles(roles: string[], userRoles: string[]): void {
if (!roles.some(role => userRoles.includes(role))) {
/**
* Failed to verify user rights
*/
throw new Exception(this.error.message, this.error.status, this.error.errorCode);
}
}
public async handle({ auth }: HttpContextContract, next: () => Promise<void>, roles: string[]) {
const { userRoles } = auth.use('api').token?.meta;
this.checkRoles(roles, userRoles);
await next();
}
}

31
app/Models/Contact.ts Normal file
View File

@ -0,0 +1,31 @@
import { DateTime } from 'luxon';
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm';
export default class Contact extends BaseModel {
@column({ isPrimary: true })
public id: number;
@column({ serializeAs: null })
user_id: string;
@column()
public email: string;
@column()
public phone_number: string;
@column()
public vk_id: string;
@column()
public twitter_id: string;
@column()
public telegram_id: string;
@column.dateTime({ autoCreate: true, serializeAs: null })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null })
public updatedAt: DateTime;
}

20
app/Models/Role.ts Normal file
View File

@ -0,0 +1,20 @@
/* eslint-disable import/no-cycle */
import { DateTime } from 'luxon';
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm';
export default class Role extends BaseModel {
@column({ isPrimary: true })
public id: number;
@column()
public name: string;
@column()
public slug: string;
@column.dateTime({ autoCreate: true, serializeAs: null })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null })
public updatedAt: DateTime;
}

View File

@ -1,3 +1,4 @@
/* eslint-disable import/no-cycle */
/* eslint-disable no-param-reassign */
import { DateTime } from 'luxon';
import {
@ -5,36 +6,46 @@ import {
beforeCreate,
beforeSave,
column,
HasOne,
hasOne,
ManyToMany,
manyToMany,
} from '@ioc:Adonis/Lucid/Orm';
import Hash from '@ioc:Adonis/Core/Hash';
import { nanoid } from 'nanoid';
import Role from './Role';
import Contact from './Contact';
export default class User extends BaseModel {
@column({ isPrimary: true })
public id: string;
@column()
public name: string;
public first_name: string;
@column()
public surname: string;
public last_name: string;
@column()
public patronymic: string;
@column()
@column({ serializeAs: null })
public login: string;
@column()
public email: string;
@column({ serializeAs: null })
public password: string;
@column.dateTime({ autoCreate: true })
@hasOne(() => Contact, {
foreignKey: 'user_id',
})
public contacts: HasOne<typeof Contact>;
@manyToMany(() => Role, {
pivotTable: 'users_roles',
})
public roles: ManyToMany<typeof Role>;
@column.dateTime({ autoCreate: true, serializeAs: null })
public createdAt: DateTime;
@column.dateTime({ autoCreate: true, autoUpdate: true })
@column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null })
public updatedAt: DateTime;
@beforeCreate()

View File

@ -0,0 +1,12 @@
import { schema } from '@ioc:Adonis/Core/Validator';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class AddRoleToUserValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
roles: schema.array().members(schema.string()),
});
public messages = {};
}

View File

@ -24,14 +24,13 @@ export default class CreateUserValidator {
* ```
*/
public schema = schema.create({
name: schema.string(),
surname: schema.string(),
patronymic: schema.string(),
login: schema.string(),
email: schema.string({}, [
rules.email(),
rules.unique({ table: 'users', column: 'email' }),
first_name: schema.string(),
last_name: schema.string(),
login: schema.string({}, [
rules.maxLength(128),
rules.unique({ table: 'users', column: 'login' }),
]),
email: schema.string({}, [rules.email(), rules.unique({ table: 'contacts', column: 'email' })]),
password: schema.string(),
});

View File

@ -5,27 +5,15 @@ export default class UpdateUserValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
name: schema.string.optional(),
surname: schema.string.optional(),
patronymic: schema.string.optional(),
login: schema.string.optional(),
first_name: schema.string.optional(),
last_name: schema.string.optional(),
login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]),
email: schema.string.optional({}, [
rules.email(),
rules.unique({ table: 'users', column: 'email' }),
rules.unique({ table: 'contacts', column: 'email' }),
]),
password: schema.string.optional(),
});
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages = {};
}

112
config/auth.ts Normal file
View File

@ -0,0 +1,112 @@
/**
* Config source: https://git.io/JY0mp
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import { AuthConfig } from '@ioc:Adonis/Addons/Auth';
/*
|--------------------------------------------------------------------------
| Authentication Mapping
|--------------------------------------------------------------------------
|
| List of available authentication mapping. You must first define them
| inside the `contracts/auth.ts` file before mentioning them here.
|
*/
const authConfig: AuthConfig = {
guard: 'api',
guards: {
/*
|--------------------------------------------------------------------------
| OAT Guard
|--------------------------------------------------------------------------
|
| OAT (Opaque access tokens) guard uses database backed tokens to authenticate
| HTTP request. This guard DOES NOT rely on sessions or cookies and uses
| Authorization header value for authentication.
|
| Use this guard to authenticate mobile apps or web clients that cannot rely
| on cookies/sessions.
|
*/
api: {
driver: 'oat',
/*
|--------------------------------------------------------------------------
| Redis provider for managing tokens
|--------------------------------------------------------------------------
|
| Uses Redis for managing tokens. We recommend using the "redis" driver
| over the "database" driver when the tokens based auth is the
| primary authentication mode.
|
| Redis ensure that all the expired tokens gets cleaned up automatically.
| Whereas with SQL, you have to cleanup expired tokens manually.
|
| The foreignKey column is used to make the relationship between the user
| and the token. You are free to use any column name here.
|
*/
tokenProvider: {
type: 'api',
driver: 'redis',
redisConnection: 'local',
foreignKey: 'user_id',
},
provider: {
/*
|--------------------------------------------------------------------------
| Driver
|--------------------------------------------------------------------------
|
| Name of the driver
|
*/
driver: 'lucid',
/*
|--------------------------------------------------------------------------
| Identifier key
|--------------------------------------------------------------------------
|
| The identifier key is the unique key on the model. In most cases specifying
| the primary key is the right choice.
|
*/
identifierKey: 'id',
/*
|--------------------------------------------------------------------------
| Uids
|--------------------------------------------------------------------------
|
| Uids are used to search a user against one of the mentioned columns. During
| login, the auth module will search the user mentioned value against one
| of the mentioned columns to find their user record.
|
*/
uids: ['email'],
/*
|--------------------------------------------------------------------------
| Model
|--------------------------------------------------------------------------
|
| The model to use for fetching or finding users. The model is imported
| lazily since the config files are read way earlier in the lifecycle
| of booting the app and the models may not be in a usable state at
| that time.
|
*/
model: () => import('App/Models/User'),
},
},
},
};
export default authConfig;

View File

@ -44,6 +44,10 @@ const databaseConfig: DatabaseConfig = {
},
migrations: {
naturalSort: true,
tableName: 'migrations',
},
seeders: {
paths: ['./database/seeders/MainSeeder'],
},
healthCheck: true,
debug: true,

48
config/redis.ts Normal file
View File

@ -0,0 +1,48 @@
/**
* Config source: https://git.io/JemcF
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import Env from '@ioc:Adonis/Core/Env';
import { RedisConfig } from '@ioc:Adonis/Addons/Redis';
/*
|--------------------------------------------------------------------------
| Redis configuration
|--------------------------------------------------------------------------
|
| Following is the configuration used by the Redis provider to connect to
| the redis server and execute redis commands.
|
| Do make sure to pre-define the connections type inside `contracts/redis.ts`
| file for AdonisJs to recognize connections.
|
| Make sure to check `contracts/redis.ts` file for defining extra connections
*/
const redisConfig: RedisConfig = {
connection: Env.get('REDIS_CONNECTION'),
connections: {
/*
|--------------------------------------------------------------------------
| The default connection
|--------------------------------------------------------------------------
|
| The main connection you want to use to execute redis commands. The same
| connection will be used by the session provider, if you rely on the
| redis driver.
|
*/
local: {
host: Env.get('REDIS_HOST'),
port: Env.get('REDIS_PORT'),
password: Env.get('REDIS_PASSWORD', ''),
db: 0,
keyPrefix: '',
},
},
};
export default redisConfig;

72
contracts/auth.ts Normal file
View File

@ -0,0 +1,72 @@
/**
* Contract source: https://git.io/JOdz5
*
* Feel free to let us know via PR, if you find something broken in this
* file.
*/
import User from 'App/Models/User'
declare module '@ioc:Adonis/Addons/Auth' {
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| The providers are used to fetch users. The Auth module comes pre-bundled
| with two providers that are `Lucid` and `Database`. Both uses database
| to fetch user details.
|
| You can also create and register your own custom providers.
|
*/
interface ProvidersList {
/*
|--------------------------------------------------------------------------
| User Provider
|--------------------------------------------------------------------------
|
| The following provider uses Lucid models as a driver for fetching user
| details from the database for authentication.
|
| You can create multiple providers using the same underlying driver with
| different Lucid models.
|
*/
user: {
implementation: LucidProviderContract<typeof User>,
config: LucidProviderConfig<typeof User>,
},
}
/*
|--------------------------------------------------------------------------
| Guards
|--------------------------------------------------------------------------
|
| The guards are used for authenticating users using different drivers.
| The auth module comes with 3 different guards.
|
| - SessionGuardContract
| - BasicAuthGuardContract
| - OATGuardContract ( Opaque access token )
|
| Every guard needs a provider for looking up users from the database.
|
*/
interface GuardsList {
/*
|--------------------------------------------------------------------------
| OAT Guard
|--------------------------------------------------------------------------
|
| OAT, stands for (Opaque access tokens) guard uses database backed tokens
| to authenticate requests.
|
*/
api: {
implementation: OATGuardContract<'user', 'api'>,
config: OATGuardConfig<'user'>,
},
}
}

12
contracts/redis.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* Contract source: https://git.io/JemcN
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
declare module '@ioc:Adonis/Addons/Redis' {
interface RedisConnectionsList {
local: RedisConnectionConfig;
}
}

View File

@ -5,12 +5,10 @@ export default class Users extends BaseSchema {
public async up() {
this.schema.createTable(this.tableName, table => {
table.string('id', 21).primary().notNullable();
table.string('name', 24).notNullable();
table.string('surname', 24).notNullable();
table.string('patronymic', 24).notNullable();
table.string('id', 21).primary();
table.string('first_name', 24).notNullable();
table.string('last_name', 24).notNullable();
table.string('login', 128).notNullable().unique();
table.string('email', 254).notNullable().unique();
table.string('password').notNullable();
table.timestamp('created_at', { useTz: true });
table.timestamp('updated_at', { useTz: true });

View File

@ -0,0 +1,19 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema';
export default class Roles extends BaseSchema {
protected tableName = 'roles';
public async up() {
this.schema.createTable(this.tableName, table => {
table.increments('id');
table.string('name');
table.string('slug');
table.timestamp('created_at', { useTz: true });
table.timestamp('updated_at', { useTz: true });
});
}
public async down() {
this.schema.dropTable(this.tableName);
}
}

View File

@ -0,0 +1,18 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema';
export default class UsersRoles extends BaseSchema {
protected tableName = 'users_roles';
public async up() {
this.schema.createTable(this.tableName, table => {
table.increments('id');
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
table.integer('role_id').unsigned().references('roles.id').onDelete('CASCADE');
table.unique(['user_id', 'role_id']);
});
}
public async down() {
this.schema.dropTable(this.tableName);
}
}

View File

@ -0,0 +1,23 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema';
export default class Contacts extends BaseSchema {
protected tableName = 'contacts';
public async up() {
this.schema.createTable(this.tableName, table => {
table.increments('id');
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
table.string('email').unique().notNullable();
table.string('phone_number').unique();
table.string('vk_id').unique();
table.string('twitter_id').unique();
table.string('telegram_id').unique();
table.timestamp('created_at', { useTz: true });
table.timestamp('updated_at', { useTz: true });
});
}
public async down() {
this.schema.dropTable(this.tableName);
}
}

View File

@ -0,0 +1,23 @@
import BaseSeeder from '@ioc:Adonis/Lucid/Seeder';
import Application from '@ioc:Adonis/Core/Application';
export default class IndexSeeder extends BaseSeeder {
private async runSeeder(seeder: { default: typeof BaseSeeder }) {
const Seeder = seeder.default;
/**
* Do not run when not in dev mode and seeder is development
* only
*/
if (Seeder.developmentOnly && !Application.inDev) {
return;
}
await new Seeder(this.client).run();
}
public async run() {
await this.runSeeder(await import('../Role'));
await this.runSeeder(await import('../User'));
}
}

22
database/seeders/Role.ts Normal file
View File

@ -0,0 +1,22 @@
/* eslint-disable class-methods-use-this */
import BaseSeeder from '@ioc:Adonis/Lucid/Seeder';
import Role from 'App/Models/Role';
export default class RoleSeeder extends BaseSeeder {
public async run() {
await Role.createMany([
{
name: 'Administrator',
slug: 'admin',
},
{
name: 'Teacher',
slug: 'teacher',
},
{
name: 'Student',
slug: 'student',
},
]);
}
}

56
database/seeders/User.ts Normal file
View File

@ -0,0 +1,56 @@
/* eslint-disable class-methods-use-this */
import BaseSeeder from '@ioc:Adonis/Lucid/Seeder';
import Role from 'App/Models/Role';
import User from 'App/Models/User';
export default class UserSeeder extends BaseSeeder {
public async run() {
/**
* Roles
*/
const roles = {
admin: await Role.findByOrFail('slug', 'admin'),
teacher: await Role.findByOrFail('slug', 'teacher'),
student: await Role.findByOrFail('slug', 'student'),
};
/**
* Administrator
*/
const user1 = new User();
user1.first_name = 'Ivan';
user1.last_name = 'Ivanov';
user1.login = 'ivan123';
user1.password = 'ivan123';
await user1.save();
await user1.related('roles').attach([roles.admin.id]);
await user1.related('contacts').create({ email: 'ivan_ivanov@ya.ru' });
/**
* Teacher
*/
const user2 = new User();
user2.first_name = 'Nikolai';
user2.last_name = 'Nikolaev';
user2.login = 'nikolai123';
user2.password = 'nikolai123';
await user2.save();
await user2.related('roles').attach([roles.teacher.id]);
await user2.related('contacts').create({ email: 'nikolai_niko123@ya.ru' });
/**
* Student
*/
const user3 = new User();
user3.first_name = 'Oleg';
user3.last_name = 'Olegov';
user3.login = 'oleg123';
user3.password = 'oleg123';
await user3.save();
await user3.related('roles').attach([roles.student.id]);
await user3.related('contacts').create({ email: 'oleg_olegov123@ya.ru' });
}
}

4
env.ts
View File

@ -25,4 +25,8 @@ export default Env.rules({
PG_USER: Env.schema.string(),
PG_PASSWORD: Env.schema.string.optional(),
PG_DB_NAME: Env.schema.string(),
REDIS_CONNECTION: Env.schema.enum(['local'] as const),
REDIS_HOST: Env.schema.string({ format: 'host' }),
REDIS_PORT: Env.schema.number(),
REDIS_PASSWORD: Env.schema.string.optional(),
});

85
package-lock.json generated
View File

@ -237,6 +237,16 @@
"jest-worker": "^27.0.6"
}
},
"@adonisjs/redis": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.0.7.tgz",
"integrity": "sha512-M9nn2uaE8IJ25mMkoPQtrsvfjewjZSfOr2F2RzySDq/ymoOQ5RcBDrgZ40E/JUqPqDOV5zmU2E0ClEwujQFPmg==",
"requires": {
"@poppinss/utils": "^3.1.3",
"@types/ioredis": "^4.26.4",
"ioredis": "^4.27.6"
}
},
"@adonisjs/repl": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@adonisjs/repl/-/repl-3.1.4.tgz",
@ -668,6 +678,14 @@
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.1.2.tgz",
"integrity": "sha512-kSJPcLO1x+oolc0R89pUl2kozldQ/fVQ1C1p5mp8fPoLdF/ZcBvckaTC2M8xXh3GYendXvCpy5m/a2eSbfgNgw=="
},
"@types/ioredis": {
"version": "4.26.5",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.26.5.tgz",
"integrity": "sha512-aP/SUSFShzoVYcHHKKEq7+kQ5YaDsMRe64rcHCp+w4Unkfw2tico1vA6siPnMDe7+jIxuZRU/h8km6uZjEtswg==",
"requires": {
"@types/node": "*"
}
},
"@types/json-schema": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz",
@ -1643,6 +1661,11 @@
"string-width": "^4.2.0"
}
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"co-compose": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/co-compose/-/co-compose-6.1.3.tgz",
@ -1938,6 +1961,11 @@
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"denque": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -3381,6 +3409,30 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
},
"ioredis": {
"version": "4.27.6",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.6.tgz",
"integrity": "sha512-6W3ZHMbpCa8ByMyC1LJGOi7P2WiOKP9B3resoZOVLDhi+6dDBOW+KNsRq3yI36Hmnb2sifCxHX+YSarTeXh48A==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"dependencies": {
"p-map": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
"integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="
}
}
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -3835,6 +3887,16 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -5266,6 +5328,24 @@
"esprima": "~4.0.0"
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@ -5824,6 +5904,11 @@
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
"dev": true
},
"standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
},
"static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",

View File

@ -29,6 +29,7 @@
"@adonisjs/auth": "^8.0.7",
"@adonisjs/core": "^5.1.9",
"@adonisjs/lucid": "^15.0.3",
"@adonisjs/redis": "^7.0.7",
"@adonisjs/repl": "^3.1.4",
"luxon": "^1.28.0",
"nanoid": "^3.1.23",

View File

@ -38,4 +38,7 @@ Server.middleware.register([() => import('@ioc:Adonis/Core/BodyParser')]);
| Route.get('dashboard', 'UserController.dashboard').middleware('auth')
|
*/
Server.middleware.registerNamed({});
Server.middleware.registerNamed({
auth: () => import('App/Middleware/Auth'),
role: () => import('App/Middleware/Role'),
});

View File

@ -23,9 +23,18 @@ import Route from '@ioc:Adonis/Core/Route';
Route.get('/', () => `API REST Server.`);
Route.group(() => {
Route.resource('users', 'UsersController').apiOnly();
// Route.get('/users', 'UsersController.index');
// Route.get('/users/:id', 'UsersController.show');
// Route.post('/users', 'UsersController.store');
// Route.put('/users/:id', 'UsersController.update');
/* Auth */
Route.post('login', 'AuthController.login');
Route.post('logout', 'AuthController.logout').middleware('auth');
/* API */
Route.group(() => {
Route.get('users', 'UsersController.index').middleware('role:admin,teacher,student');
Route.get('users/:id', 'UsersController.show').middleware('role:admin,teacher,student');
Route.post('users', 'UsersController.store').middleware('role:admin');
Route.patch('users/:id', 'UsersController.update').middleware('role:admin');
Route.delete('users', 'UsersController.destroy').middleware('role:admin');
Route.post('users/:id/roles', 'RolesController.store').middleware('role:admin');
Route.delete('users/:id/roles', 'RolesController.destroy').middleware('role:admin');
}).middleware('auth');
}).prefix('api');

View File

@ -28,7 +28,9 @@
"types": [
"@adonisjs/core",
"@adonisjs/repl",
"@adonisjs/lucid"
"@adonisjs/lucid",
"@adonisjs/auth",
"@adonisjs/redis"
]
}
}