diff --git a/Gemfile b/Gemfile index 077af24..d958f48 100644 --- a/Gemfile +++ b/Gemfile @@ -66,3 +66,4 @@ group :test do end gem "devise", "~> 4.9" +gem "avo", ">= 3.2" diff --git a/Gemfile.lock b/Gemfile.lock index 5eb9b74..7411372 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,9 +100,27 @@ GIT GEM remote: https://rubygems.org/ specs: + active_link_to (1.0.5) + actionpack + addressable addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) + avo (3.15.1) + actionview (>= 6.1) + active_link_to + activerecord (>= 6.1) + activesupport (>= 6.1) + addressable + docile + inline_svg + meta-tags + pagy (>= 7.0.0) + prop_initializer (>= 0.2.0) + turbo-rails (>= 2.0.0) + turbo_power (>= 0.6.0) + view_component (>= 3.7.0) + zeitwerk (>= 2.6.12) base64 (0.2.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) @@ -136,6 +154,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) + docile (1.4.1) dotenv (3.1.4) drb (2.2.1) ed25519 (1.3.0) @@ -153,6 +172,9 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.8.0) irb (1.14.1) rdoc (>= 4.0.0) @@ -184,6 +206,9 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + meta-tags (2.22.1) + actionpack (>= 6.0.0, < 8.1) + method_source (1.1.0) mini_mime (1.1.5) mini_portile2 (2.8.8) minitest (5.25.4) @@ -207,10 +232,13 @@ GEM racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.1) + pagy (9.3.2) parallel (1.26.3) parser (3.3.6.0) ast (~> 2.4.1) racc + prop_initializer (0.2.0) + zeitwerk (>= 2.6.18) propshaft (1.1.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -324,6 +352,8 @@ GEM turbo-rails (2.0.11) actionpack (>= 6.0.0) railties (>= 6.0.0) + turbo_power (0.6.2) + turbo-rails (>= 1.3.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.1.2) @@ -331,6 +361,10 @@ GEM unicode-emoji (4.0.4) uri (1.0.2) useragent (0.16.11) + view_component (3.20.0) + activesupport (>= 5.2.0, < 8.1) + concurrent-ruby (~> 1.0) + method_source (~> 1.0) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -350,6 +384,7 @@ PLATFORMS arm64-darwin-24 DEPENDENCIES + avo (>= 3.2) bootsnap brakeman capybara diff --git a/app/avo/resources/user.rb b/app/avo/resources/user.rb new file mode 100644 index 0000000..8b0a161 --- /dev/null +++ b/app/avo/resources/user.rb @@ -0,0 +1,16 @@ +class Avo::Resources::User < Avo::BaseResource + # self.includes = [] + # self.attachments = [] + # self.search = { + # query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) } + # } + + def fields + field :email, as: :gravatar + field :name, as: :text + field :email, as: :text + field :created_at, as: :date, readonly: true + field :updated_at, as: :date, readonly: true + field :id, as: :text, readonly: true + end +end diff --git a/app/controllers/api/v1/application_controller.rb b/app/controllers/api/v1/application_controller.rb new file mode 100644 index 0000000..3b2e1c3 --- /dev/null +++ b/app/controllers/api/v1/application_controller.rb @@ -0,0 +1,9 @@ +module Api + module V1 + class ApplicationController < ActionController::API + rescue_from ActiveRecord::RecordNotFound do |e| + render json: { error: e.message }, status: :not_found + end + end + end +end diff --git a/app/controllers/avo/users_controller.rb b/app/controllers/avo/users_controller.rb new file mode 100644 index 0000000..a9987c6 --- /dev/null +++ b/app/controllers/avo/users_controller.rb @@ -0,0 +1,4 @@ +# This controller has been generated to enable Rails' resource routes. +# More information on https://docs.avohq.io/3.0/controllers.html +class Avo::UsersController < Avo::ResourcesController +end diff --git a/app/models/admin.rb b/app/models/admin.rb new file mode 100644 index 0000000..7f0c40f --- /dev/null +++ b/app/models/admin.rb @@ -0,0 +1,6 @@ +class Admin < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, # :registerable, + :recoverable, :rememberable, :validatable +end diff --git a/app/models/user.rb b/app/models/user.rb index 4756799..dcb5f12 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,4 +3,6 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + validates :name, presence: true end diff --git a/config/initializers/api.rb b/config/initializers/api.rb new file mode 100644 index 0000000..377d821 --- /dev/null +++ b/config/initializers/api.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + # config.api_version = 'v1' +end diff --git a/config/initializers/avo.rb b/config/initializers/avo.rb new file mode 100644 index 0000000..665b2f7 --- /dev/null +++ b/config/initializers/avo.rb @@ -0,0 +1,163 @@ +# For more information regarding these settings check out our docs https://docs.avohq.io +# The values disaplayed here are the default ones. Uncomment and change them to fit your needs. +Avo.configure do |config| + ## == Routing == + config.root_path = '/avo' + # used only when you have custom `map` configuration in your config.ru + # config.prefix_path = "/internal" + + # Sometimes you might want to mount Avo's engines yourself. + # https://docs.avohq.io/3.0/routing.html + # config.mount_avo_engines = true + + # Where should the user be redirected when visiting the `/avo` url + # config.home_path = nil + + ## == Licensing == + # config.license_key = ENV['AVO_LICENSE_KEY'] + + ## == Set the context == + config.set_context do + # Return a context object that gets evaluated within Avo::ApplicationController + end + + ## == Authentication == + # config.current_user_method = :current_user + # config.authenticate_with do + # end + + ## == Authorization == + # config.is_admin_method = :is_admin + # config.is_developer_method = :is_developer + # config.authorization_methods = { + # index: 'index?', + # show: 'show?', + # edit: 'edit?', + # new: 'new?', + # update: 'update?', + # create: 'create?', + # destroy: 'destroy?', + # search: 'search?', + # } + # config.raise_error_on_missing_policy = false + config.authorization_client = nil + config.explicit_authorization = true + + ## == Localization == + # config.locale = 'en-US' + + ## == Resource options == + # config.resource_controls_placement = :right + # config.model_resource_mapping = {} + # config.default_view_type = :table + # config.per_page = 24 + # config.per_page_steps = [12, 24, 48, 72] + # config.via_per_page = 8 + # config.id_links_to_resource = false + # config.pagination = -> do + # { + # type: :default, + # size: 9, # `[1, 2, 2, 1]` for pagy < 9.0 + # } + # end + + ## == Response messages dismiss time == + # config.alert_dismiss_time = 5000 + + + ## == Number of search results to display == + # config.search_results_count = 8 + + ## == Associations lookup list limit == + # config.associations_lookup_list_limit = 1000 + + ## == Cache options == + ## Provide a lambda to customize the cache store used by Avo. + ## We compute the cache store by default, this is NOT the default, just an example. + # config.cache_store = -> { + # ActiveSupport::Cache.lookup_store(:solid_cache_store) + # } + # config.cache_resources_on_index_view = true + ## permanent enable or disable cache_resource_filters, default value is false + # config.cache_resource_filters = false + ## provide a lambda to enable or disable cache_resource_filters per user/resource. + # config.cache_resource_filters = -> { current_user.cache_resource_filters? } + + ## == Turbo options == + # config.turbo = -> do + # { + # instant_click: true + # } + # end + + ## == Logger == + # config.logger = -> { + # file_logger = ActiveSupport::Logger.new(Rails.root.join("log", "avo.log")) + # + # file_logger.datetime_format = "%Y-%m-%d %H:%M:%S" + # file_logger.formatter = proc do |severity, time, progname, msg| + # "[Avo] #{time}: #{msg}\n".tap do |i| + # puts i + # end + # end + # + # file_logger + # } + + ## == Customization == + config.click_row_to_view_record = true + # config.app_name = 'Avocadelicious' + # config.timezone = 'UTC' + # config.currency = 'USD' + # config.hide_layout_when_printing = false + # config.full_width_container = false + # config.full_width_index_view = false + # config.search_debounce = 300 + # config.view_component_path = "app/components" + # config.display_license_request_timeout_error = true + # config.disabled_features = [] + # config.buttons_on_form_footers = true + # config.field_wrapper_layout = true + # config.resource_parent_controller = "Avo::ResourcesController" + # config.first_sorting_option = :desc # :desc or :asc + + ## == Branding == + # config.branding = { + # colors: { + # background: "248 246 242", + # 100 => "#CEE7F8", + # 400 => "#399EE5", + # 500 => "#0886DE", + # 600 => "#066BB2", + # }, + # chart_colors: ["#0B8AE2", "#34C683", "#2AB1EE", "#34C6A8"], + # logo: "/avo-assets/logo.png", + # logomark: "/avo-assets/logomark.png", + # placeholder: "/avo-assets/placeholder.svg", + # favicon: "/avo-assets/favicon.ico" + # } + + ## == Breadcrumbs == + # config.display_breadcrumbs = true + # config.set_initial_breadcrumbs do + # add_breadcrumb "Home", '/avo' + # end + + ## == Menus == + # config.main_menu = -> { + # section "Dashboards", icon: "avo/dashboards" do + # all_dashboards + # end + + # section "Resources", icon: "avo/resources" do + # all_resources + # end + + # section "Tools", icon: "avo/tools" do + # all_tools + # end + # } + # config.profile_menu = -> { + # link "Profile", path: "/avo/profile", icon: "heroicons/outline/user-circle" + # } +end diff --git a/config/initializers/generators.rb b/config/initializers/generators.rb index 1111e23..2384365 100644 --- a/config/initializers/generators.rb +++ b/config/initializers/generators.rb @@ -1,3 +1,16 @@ Rails.application.config.generators do |g| g.orm :active_record, primary_key_type: :string + + g.after_generate do |files| + next if files.grep(/controller\.rb/).empty? + + controller_name = files.grep(/controller\.rb/).first.split('/').last.gsub('_controller.rb', '') + + Rails::Generators.invoke "scaffold_controller", [ + "api/v1/#{controller_name}", + *ARGV.drop(1), + "--api", + "--force" + ] + end end diff --git a/config/routes.rb b/config/routes.rb index be25e8b..89754a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,12 @@ Rails.application.routes.draw do + + devise_for :users + devise_for :admins + authenticate :admin do + mount Avo:: Engine, at: Avo.configuration.root_path + end + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20241207205829_devise_create_admins.rb b/db/migrate/20241207205829_devise_create_admins.rb new file mode 100644 index 0000000..0d2ed82 --- /dev/null +++ b/db/migrate/20241207205829_devise_create_admins.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class DeviseCreateAdmins < ActiveRecord::Migration[8.1] + def change + create_table :admins, force: true, id: false do |t| + t.primary_key :id, :string, default: -> { "ULID()" } + + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + t.timestamps null: false + end + + add_index :admins, :email, unique: true + add_index :admins, :reset_password_token, unique: true + # add_index :admins, :confirmation_token, unique: true + # add_index :admins, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 0f630f5..ec5e4f9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2024_12_07_165010) do +ActiveRecord::Schema[8.1].define(version: 2024_12_07_205829) do + create_table "admins", id: :string, default: -> { "ULID()" }, force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_admins_on_email", unique: true + t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true + end + create_table "users", id: :string, default: -> { "ULID()" }, force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false diff --git a/lib/templates/rails/scaffold_controller/api_controller.rb.tt b/lib/templates/rails/scaffold_controller/api_controller.rb.tt new file mode 100644 index 0000000..e742714 --- /dev/null +++ b/lib/templates/rails/scaffold_controller/api_controller.rb.tt @@ -0,0 +1,57 @@ +<% module_namespacing do -%> +class <%= controller_class_name %>Controller < ApplicationController + before_action :set_<%= singular_table_name %>, only: %i[ show update destroy ] + + # GET <%= route_url %> + def index + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> + + render json: <%= "@#{plural_table_name}" %> + end + + # GET <%= route_url %>/1 + def show + render json: <%= "@#{singular_table_name}" %> + end + + # POST <%= route_url %> + def create + @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> + + if @<%= orm_instance.save %> + render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + if @<%= orm_instance.update("#{singular_table_name}_params") %> + render json: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # DELETE <%= route_url %>/1 + def destroy + @<%= orm_instance.destroy %> + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_<%= singular_table_name %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params.expect(:id)") %> + end + + # Only allow a list of trusted parameters through. + def <%= "#{singular_table_name}_params" %> + <%- if attributes_names.empty? -%> + params.fetch(:<%= singular_table_name %>, {}) + <%- else -%> + params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ]) + <%- end -%> + end +end +<% end -%> diff --git a/lib/templates/rails/scaffold_controller/controller.rb.tt b/lib/templates/rails/scaffold_controller/controller.rb.tt new file mode 100644 index 0000000..3efa3dc --- /dev/null +++ b/lib/templates/rails/scaffold_controller/controller.rb.tt @@ -0,0 +1,64 @@ +<% module_namespacing do -%> +class <%= controller_class_name %>Controller < ApplicationController + before_action :set_<%= singular_table_name %>, only: %i[ show edit update destroy ] + + # GET <%= route_url %> + def index + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> + end + + # GET <%= route_url %>/1 + def show + end + + # GET <%= route_url %>/new + def new + @<%= singular_table_name %> = <%= orm_class.build(class_name) %> + end + + # GET <%= route_url %>/1/edit + def edit + end + + # POST <%= route_url %> + def create + @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> + + if @<%= orm_instance.save %> + redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully created.") %> + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + if @<%= orm_instance.update("#{singular_table_name}_params") %> + redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully updated.") %>, status: :see_other + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE <%= route_url %>/1 + def destroy + @<%= orm_instance.destroy %> + redirect_to <%= index_helper %>_path, notice: <%= %("#{human_name} was successfully destroyed.") %>, status: :see_other + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_<%= singular_table_name %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params.expect(:id)") %> + end + + # Only allow a list of trusted parameters through. + def <%= "#{singular_table_name}_params" %> + <%- if attributes_names.empty? -%> + params.fetch(:<%= singular_table_name %>, {}) + <%- else -%> + params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ]) + <%- end -%> + end +end +<% end -%> diff --git a/test/fixtures/admins.yml b/test/fixtures/admins.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/admins.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/models/admin_test.rb b/test/models/admin_test.rb new file mode 100644 index 0000000..33dd194 --- /dev/null +++ b/test/models/admin_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AdminTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end