diff --git a/.adonisrc.json b/.adonisrc.json index 5089086..448017b 100644 --- a/.adonisrc.json +++ b/.adonisrc.json @@ -31,7 +31,8 @@ "@adonisjs/view", "@adonisjs/shield", "@eidellev/inertia-adonisjs", - "@adonisjs/lucid" + "@adonisjs/lucid", + "@adonisjs/auth" ], "metaFiles": [ { diff --git a/.gitignore b/.gitignore index e60b7a8..1703a62 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,5 @@ dist .yarn/install-state.gz .pnp.* -config/database.ts \ No newline at end of file +config/database.ts +tmp \ No newline at end of file diff --git a/app/Controllers/Http/AuthController.ts b/app/Controllers/Http/AuthController.ts new file mode 100644 index 0000000..0205b7b --- /dev/null +++ b/app/Controllers/Http/AuthController.ts @@ -0,0 +1,3 @@ +// import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class AuthController {} diff --git a/app/Controllers/Http/EventsController.ts b/app/Controllers/Http/EventsController.ts new file mode 100644 index 0000000..d92880a --- /dev/null +++ b/app/Controllers/Http/EventsController.ts @@ -0,0 +1,17 @@ +import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class EventsController { + public async index({}: HttpContextContract) {} + + public async create({}: HttpContextContract) {} + + public async store({}: HttpContextContract) {} + + public async show({}: HttpContextContract) {} + + public async edit({}: HttpContextContract) {} + + public async update({}: HttpContextContract) {} + + public async destroy({}: HttpContextContract) {} +} diff --git a/app/Controllers/Http/HomeController.ts b/app/Controllers/Http/HomeController.ts new file mode 100644 index 0000000..2556f61 --- /dev/null +++ b/app/Controllers/Http/HomeController.ts @@ -0,0 +1,9 @@ +import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class HomeController { + + public async index({ inertia }: HttpContextContract) { + + return inertia.render('Home') + } +} diff --git a/app/Controllers/Http/UsersController.ts b/app/Controllers/Http/UsersController.ts new file mode 100644 index 0000000..242cf1a --- /dev/null +++ b/app/Controllers/Http/UsersController.ts @@ -0,0 +1,17 @@ +import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class UsersController { + public async index({}: HttpContextContract) {} + + public async create({}: HttpContextContract) {} + + public async store({}: HttpContextContract) {} + + public async show({}: HttpContextContract) {} + + public async edit({}: HttpContextContract) {} + + public async update({}: HttpContextContract) {} + + public async destroy({}: HttpContextContract) {} +} diff --git a/app/Middleware/Auth.ts b/app/Middleware/Auth.ts new file mode 100644 index 0000000..1a26ad0 --- /dev/null +++ b/app/Middleware/Auth.ts @@ -0,0 +1,76 @@ +import { AuthenticationException } from '@adonisjs/auth/build/standalone' +import type { GuardsList } from '@ioc:Adonis/Addons/Auth' +import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +/** + * 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 + + for (let guard of guards) { + guardLastAttempted = guard + + if (await auth.use(guard).check()) { + /** + * Instruct auth to use the given guard as the default guard for + * the rest of the request, since the user authenticated + * succeeded here + */ + auth.defaultGuard = guard + return true + } + } + + /** + * Unable to authenticate using any guard + */ + throw new AuthenticationException( + 'Unauthorized access', + 'E_UNAUTHORIZED_ACCESS', + guardLastAttempted, + this.redirectTo, + ) + } + + /** + * Handle request + */ + public async handle ( + { auth }: HttpContextContract, + next: () => Promise, + 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() + } +} diff --git a/app/Middleware/SilentAuth.ts b/app/Middleware/SilentAuth.ts new file mode 100644 index 0000000..5d3ac8f --- /dev/null +++ b/app/Middleware/SilentAuth.ts @@ -0,0 +1,21 @@ +import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +/** + * Silent auth middleware can be used as a global middleware to silent check + * if the user is logged-in or not. + * + * The request continues as usual, even when the user is not logged-in. + */ +export default class SilentAuthMiddleware { + /** + * Handle request + */ + public async handle({ auth }: HttpContextContract, next: () => Promise) { + /** + * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be + * set to the instance of the currently logged in user. + */ + await auth.check() + await next() + } +} diff --git a/app/Models/User.ts b/app/Models/User.ts new file mode 100644 index 0000000..d74b523 --- /dev/null +++ b/app/Models/User.ts @@ -0,0 +1,33 @@ +import { DateTime } from 'luxon' +import Hash from '@ioc:Adonis/Core/Hash' +import { column, beforeSave, BaseModel } from '@ioc:Adonis/Lucid/Orm' + +export default class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @column({ serializeAs: null }) + public password: string + + @column() + public isAdmin: boolean + + @column() + public rememberMeToken: string | null + + @column.dateTime({ autoCreate: true }) + public createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updatedAt: DateTime + + @beforeSave() + public static async hashPassword (user: User) { + if (user.$dirty.password) { + user.password = await Hash.make(user.password) + } + } +} diff --git a/config/auth.ts b/config/auth.ts new file mode 100644 index 0000000..d471560 --- /dev/null +++ b/config/auth.ts @@ -0,0 +1,86 @@ +/** + * Config source: https://git.io/JY0mp + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ + +import type { 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: 'web', + guards: { + /* + |-------------------------------------------------------------------------- + | Web Guard + |-------------------------------------------------------------------------- + | + | Web guard uses classic old school sessions for authenticating users. + | If you are building a standard web application, it is recommended to + | use web guard with session driver + | + */ + web: { + driver: 'session', + + 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: ['username'], + + /* + |-------------------------------------------------------------------------- + | 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 diff --git a/contracts/auth.ts b/contracts/auth.ts new file mode 100644 index 0000000..1fe370b --- /dev/null +++ b/contracts/auth.ts @@ -0,0 +1,73 @@ +/** + * 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 + config: LucidProviderConfig + } + } + + /* + |-------------------------------------------------------------------------- + | 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 { + /* + |-------------------------------------------------------------------------- + | Web Guard + |-------------------------------------------------------------------------- + | + | The web guard uses sessions for maintaining user login state. It uses + | the `user` provider for fetching user details. + | + */ + web: { + implementation: SessionGuardContract<'user', 'web'> + config: SessionGuardConfig<'user'> + client: SessionClientContract<'user'> + } + } +} diff --git a/database/migrations/1688389052773_users.ts b/database/migrations/1688389052773_users.ts new file mode 100644 index 0000000..9abe95e --- /dev/null +++ b/database/migrations/1688389052773_users.ts @@ -0,0 +1,25 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema' + +export default class extends BaseSchema { + protected tableName = 'users' + + public async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id').primary() + table.string('username', 255).notNullable().unique() + table.string('password', 180).notNullable() + table.string('remember_me_token').nullable() + table.boolean('is_admin').notNullable().defaultTo(false) + + /** + * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL + */ + table.timestamp('created_at', { useTz: true }).notNullable() + table.timestamp('updated_at', { useTz: true }).notNullable() + }) + } + + public async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/database/seeders/User.ts b/database/seeders/User.ts new file mode 100644 index 0000000..a78b101 --- /dev/null +++ b/database/seeders/User.ts @@ -0,0 +1,18 @@ +import BaseSeeder from '@ioc:Adonis/Lucid/Seeder' +import User from 'App/Models/User' + +export default class extends BaseSeeder { + public async run () { + await User.createMany([ + { + username: 'admin', + password: 'initialPass', + isAdmin: true + }, + { + username: "firstUser", + password: "firstPass", + } + ]) + } +} diff --git a/package-lock.json b/package-lock.json index fc9fc09..07e8bec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "enzos-events", "version": "1.0.0", "dependencies": { + "@adonisjs/auth": "^8.2.3", "@adonisjs/core": "^5.9.0", "@adonisjs/lucid": "^18.4.0", "@adonisjs/repl": "^3.1.11", @@ -147,6 +148,50 @@ "@adonisjs/core": "^5.1.0" } }, + "node_modules/@adonisjs/auth": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@adonisjs/auth/-/auth-8.2.3.tgz", + "integrity": "sha512-js9e8AHEsNC/8MGvho6MgL+uXr8SlhTg9MJJDWQBBiqsKkT7+H7NMP/pLbuSzYaaf40t2u/OXfq6wXuIC5ZYvw==", + "dependencies": { + "@poppinss/hooks": "^5.0.3", + "@poppinss/utils": "^5.0.0", + "luxon": "^3.0.4" + }, + "peerDependencies": { + "@adonisjs/core": "^5.7.1", + "@adonisjs/i18n": "^1.5.0", + "@adonisjs/lucid": "^18.0.0", + "@adonisjs/redis": "^7.2.0", + "@adonisjs/session": "^6.2.0" + }, + "peerDependenciesMeta": { + "@adonisjs/i18n": { + "optional": true + }, + "@adonisjs/lucid": { + "optional": true + }, + "@adonisjs/redis": { + "optional": true + }, + "@adonisjs/session": { + "optional": true + } + } + }, + "node_modules/@adonisjs/auth/node_modules/@poppinss/hooks": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@poppinss/hooks/-/hooks-5.0.3.tgz", + "integrity": "sha512-M5a151VUl+RslVP5qwDW+u+0VmzKt5Nfplzdx2nrtXol3yVlLN3u2Jp6UADESid3DDI7IRHmFrA3sQusey3eUA==", + "peerDependencies": { + "@adonisjs/application": ">=4.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/application": { + "optional": true + } + } + }, "node_modules/@adonisjs/bodyparser": { "version": "8.1.8", "resolved": "https://registry.npmjs.org/@adonisjs/bodyparser/-/bodyparser-8.1.8.tgz", @@ -16834,6 +16879,24 @@ "slash": "^3.0.0" } }, + "@adonisjs/auth": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@adonisjs/auth/-/auth-8.2.3.tgz", + "integrity": "sha512-js9e8AHEsNC/8MGvho6MgL+uXr8SlhTg9MJJDWQBBiqsKkT7+H7NMP/pLbuSzYaaf40t2u/OXfq6wXuIC5ZYvw==", + "requires": { + "@poppinss/hooks": "^5.0.3", + "@poppinss/utils": "^5.0.0", + "luxon": "^3.0.4" + }, + "dependencies": { + "@poppinss/hooks": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@poppinss/hooks/-/hooks-5.0.3.tgz", + "integrity": "sha512-M5a151VUl+RslVP5qwDW+u+0VmzKt5Nfplzdx2nrtXol3yVlLN3u2Jp6UADESid3DDI7IRHmFrA3sQusey3eUA==", + "requires": {} + } + } + }, "@adonisjs/bodyparser": { "version": "8.1.8", "resolved": "https://registry.npmjs.org/@adonisjs/bodyparser/-/bodyparser-8.1.8.tgz", diff --git a/package.json b/package.json index b19b4a5..22bb999 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "youch-terminal": "^2.2.1" }, "dependencies": { + "@adonisjs/auth": "^8.2.3", "@adonisjs/core": "^5.9.0", "@adonisjs/lucid": "^18.4.0", "@adonisjs/repl": "^3.1.11", diff --git a/resources/js/pages/Events/EventsList.vue b/resources/js/pages/Events/EventsIndex.vue similarity index 100% rename from resources/js/pages/Events/EventsList.vue rename to resources/js/pages/Events/EventsIndex.vue diff --git a/resources/js/pages/Home.vue b/resources/js/pages/Home.vue index 2c38782..cd942ae 100644 --- a/resources/js/pages/Home.vue +++ b/resources/js/pages/Home.vue @@ -1,5 +1,7 @@