├── log └── .keep ├── script └── .keep ├── storage └── .keep ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep ├── vendor └── .keep ├── lib └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ └── .keep ├── system │ └── .keep ├── controllers │ ├── .keep │ └── settings_controller_test.rb ├── integration │ └── .keep ├── fixtures │ └── files │ │ └── .keep ├── test_helper.rb └── application_system_test_case.rb ├── .ruby-version ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── images │ │ └── .keep │ └── stylesheets │ │ ├── safelist.txt │ │ └── application.tailwind.css ├── models │ ├── concerns │ │ └── .keep │ └── application_record.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── dashboard_controller.rb │ ├── application_controller.rb │ └── authentication_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── pwa │ │ ├── manifest.json.erb │ │ └── service-worker.js │ ├── shared │ │ └── _flash.html.erb │ ├── authentication │ │ └── login.html.erb │ ├── dashboard │ │ └── index.html.erb │ └── clients │ │ └── index.html.erb ├── helpers │ └── application_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ ├── application.js │ │ ├── index.js │ │ ├── dropdown_controller.js │ │ ├── flash_controller.js │ │ ├── client_result_controller.js │ │ └── new_client_controller.js ├── mailers │ └── application_mailer.rb ├── jobs │ └── application_job.rb └── services │ └── mikrotik_api_service.rb ├── .node-version ├── public ├── icon.png ├── robots.txt ├── icon.svg ├── 404.html ├── 400.html ├── 406-unsupported-browser.html ├── 500.html └── 422.html ├── .kamal ├── hooks │ ├── docker-setup.sample │ ├── post-proxy-reboot.sample │ ├── pre-proxy-reboot.sample │ ├── post-app-boot.sample │ ├── pre-app-boot.sample │ ├── post-deploy.sample │ ├── pre-connect.sample │ ├── pre-build.sample │ └── pre-deploy.sample └── secrets ├── bin ├── rake ├── thrust ├── jobs ├── rails ├── brakeman ├── rubocop ├── dev ├── docker-entrypoint ├── kamal ├── setup └── bundle ├── Procfile.dev ├── config ├── environment.rb ├── boot.rb ├── initializers │ ├── assets.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── cache.yml ├── queue.yml ├── recurring.yml ├── credentials.yml.enc ├── cable.yml ├── routes.rb ├── storage.yml ├── database.yml ├── application.rb ├── puma.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── deploy.yml └── locales │ ├── zh.yml │ ├── ko.yml │ ├── ja.yml │ └── en.yml ├── .devcontainer ├── Dockerfile ├── compose.yaml └── devcontainer.json ├── .env.example ├── config.ru ├── .idea ├── .gitignore ├── vcs.xml ├── misc.xml └── modules.xml ├── .erb_lint.yml ├── Rakefile ├── .rubocop_migration.yml ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitattributes ├── docker-compose.yaml ├── db ├── seeds.rb ├── cable_schema.rb ├── cache_schema.rb ├── schema.rb └── queue_schema.rb ├── package.json ├── .rubocop_custom_layout_space_inside_percent_literal_brackets.rb ├── .rubocop.yml ├── .gitignore ├── .dockerignore ├── .rubocop_security.yml ├── .rubocop_bundler.yml ├── Gemfile ├── .rubocop_metrics.yml ├── .rubocop_gemspec.yml ├── Dockerfile └── .rubocop_naming.yml /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.2 2 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 23.10.0 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/safelist.txt: -------------------------------------------------------------------------------- 1 | bg-green-500 2 | bg-red-500 3 | text-white 4 | opacity-90 -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyon/easy_wg_mikrotik/HEAD/public/icon.png -------------------------------------------------------------------------------- /.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-app-boot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-app-boot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: env RUBY_DEBUG_OPEN=true bin/rails server 2 | js: yarn build --watch 3 | css: yarn build:css --watch 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Entry point for the build script in your package.json 2 | import "@hotwired/turbo-rails" 3 | import "./controllers" 4 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version 2 | ARG RUBY_VERSION=3.4.2 3 | FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Default locale setting (ko, en, zh, ja) 2 | DEFAULT_LOCALE=ko 3 | 4 | # MikroTik router default connection settings 5 | MIKROTIK_HOST=192.168.88.1 6 | MIKROTIK_PORT=8728 -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 디폴트 무시된 파일 2 | /shelf/ 3 | /workspace.xml 4 | # 에디터 기반 HTTP 클라이언트 요청 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.erb_lint.yml: -------------------------------------------------------------------------------- 1 | # .erb_lint.yml 2 | linters: 3 | RuboCop: 4 | enabled: true 5 | rubocop_config: .rubocop.yml 6 | SpaceInHtmlTag: 7 | enabled: false 8 | RequireInputAutocomplete: 9 | enabled: false -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /.rubocop_migration.yml: -------------------------------------------------------------------------------- 1 | # Department 'Migration' (1): 수정 2 | # Supports --autocorrect 3 | Migration/DepartmentName: 4 | Description: Check that cop names in rubocop:disable (etc) comments are given with 5 | department name. 6 | Enabled: true 7 | VersionAdded: '0.75' -------------------------------------------------------------------------------- /app/controllers/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class DashboardController < ApplicationController 2 | before_action :require_mikrotik_login 3 | 4 | def index 5 | @mikrotik_host = session[:mikrotik_host] 6 | @mikrotik_user = session[:mikrotik_user] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if gem list --no-installed --exact --silent foreman; then 4 | echo "Installing foreman..." 5 | gem install foreman 6 | fi 7 | 8 | # Default to port 3000 if not specified 9 | export PORT="${PORT:-3000}" 10 | 11 | exec foreman start -f Procfile.dev --env /dev/null "$@" 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /config/cache.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | store_options: 3 | # Cap age of oldest cache entry to fulfill retention policies 4 | # max_age: <%= 60.days.to_i %> 5 | max_size: <%= 256.megabytes %> 6 | namespace: <%= Rails.env %> 7 | 8 | development: 9 | <<: *default 10 | 11 | test: 12 | <<: *default 13 | 14 | production: 15 | database: cache 16 | <<: *default 17 | -------------------------------------------------------------------------------- /config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: 3 | - polling_interval: 1 4 | batch_size: 500 5 | workers: 6 | - queues: "*" 7 | threads: 3 8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> 9 | polling_interval: 0.1 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | 17 | production: 18 | <<: *default 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLES (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Enable jemalloc for reduced memory usage and latency. 4 | if [ -z "${LD_PRELOAD+x}" ]; then 5 | LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) 6 | export LD_PRELOAD 7 | fi 8 | 9 | # If running the rails server then create or migrate existing database 10 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then 11 | ./bin/rails db:prepare 12 | fi 13 | 14 | exec "${@}" 15 | -------------------------------------------------------------------------------- /test/controllers/settings_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SettingsControllerTest < ActionDispatch::IntegrationTest 4 | test "should get dashboard" do 5 | get dashboard_url 6 | assert_response :redirect 7 | end 8 | 9 | test "should get login" do 10 | get login_url 11 | assert_response :success 12 | end 13 | 14 | test "should get logout when logged in" do 15 | delete logout_url 16 | assert_response :redirect 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | module ActiveSupport 6 | class TestCase 7 | # Run tests in parallel with specified workers 8 | parallelize(workers: :number_of_processors) 9 | 10 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 11 | fixtures :all 12 | 13 | # Add more helper methods to be used by all tests here... 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | easy_wg_mikrotik: 3 | restart: unless-stopped 4 | image: easy_wg_mikrotik 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: easy_wg_mikrotik 9 | ports: 10 | - "3000:3000" 11 | environment: 12 | RAILS_ENV: development 13 | MIKROTIK_HOST: 192.168.88.1 14 | MIKROTIK_PORT: 8728 15 | # Default locale setting (ko, en, zh, ja) 16 | DEFAULT_LOCALE: ko 17 | TZ: "Asia/Seoul" 18 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should ensure the existence of records required to run the application in every environment (production, 2 | # development, test). The code here should be idempotent so that it can be executed at any point in every environment. 3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 4 | # 5 | # Example: 6 | # 7 | # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| 8 | # MovieGenre.find_or_create_by!(name: genre_name) 9 | # end 10 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | if ENV["CAPYBARA_SERVER_PORT"] 5 | served_by host: "rails-app", port: ENV["CAPYBARA_SERVER_PORT"] 6 | 7 | driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ], options: { 8 | browser: :remote, 9 | url: "http://#{ENV["SELENIUM_HOST"]}:4444" 10 | } 11 | else 12 | driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/pwa/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= t('app.name') %>", 3 | "icons": [ 4 | { 5 | "src": "/icon.png", 6 | "type": "image/png", 7 | "sizes": "512x512" 8 | }, 9 | { 10 | "src": "/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512", 13 | "purpose": "maskable" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "scope": "/", 19 | "description": "<%= t('app.description') %>", 20 | "theme_color": "red", 21 | "background_color": "red" 22 | } 23 | -------------------------------------------------------------------------------- /config/recurring.yml: -------------------------------------------------------------------------------- 1 | # examples: 2 | # periodic_cleanup: 3 | # class: CleanSoftDeletedRecordsJob 4 | # queue: background 5 | # args: [ 1000, { batch_size: 500 } ] 6 | # schedule: every hour 7 | # periodic_cleanup_with_command: 8 | # command: "SoftDeletedRecord.due.delete_all" 9 | # priority: 2 10 | # schedule: at 5am every day 11 | 12 | production: 13 | clear_solid_queue_finished_jobs: 14 | command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" 15 | schedule: every hour at minute 12 16 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | eQV8JZZ31ipHy1ZFXjg2QO9GIlqlBYZE98SE1UaULzAjOFjHjaXHPVLU8Rc2q4OznVFBWFOAR9VlSxzCwgEiUU5w71VliP/37MdAhW/mXRMkU3RN0+f0s8mq42KP7lpEmyaBsQ3G8Kci5a61SyEH9khT8sDvCXawxWg2fzeAXSQ0vyTM/S/qQxpHzf0paKFEya8wfTyLpkyy6RzuCAVgxxOXND0Pv+ty498RJhQEpUHxG98ZLJu5iZuCjx7wtAj5uUu47t5YNKxIDC7uJ3UjHRCOcs1ufNK+v/P07ZKjCOZQ57CnOhVlxHESYZvTcxVgej39POzwdwV0lzXQ8LaAizZys5JBp12Aoc/JRHoHDWVskYIqAof/2idgcAi9sABgUDtqX1pcknw8LpmCd0jEVcAEfaq0GhP7W4du0SZp6w8iPnzhKkqOvTNynqAR7MoGiePCh/whdWtYZ6mt4gdXVDdTScblH71QayNR3FJl59c8AJaEH7rQ77qO--nMxyF94XyXgdx1kz--So4DMOQpQP3fFCo8dTCM3Q== -------------------------------------------------------------------------------- /db/cable_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_cable_messages", force: :cascade do |t| 3 | t.binary "channel", limit: 1024, null: false 4 | t.binary "payload", limit: 536870912, null: false 5 | t.datetime "created_at", null: false 6 | t.integer "channel_hash", limit: 8, null: false 7 | t.index ["channel"], name: "index_solid_cable_messages_on_channel" 8 | t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" 9 | t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "devDependencies": { 5 | "esbuild": "^0.25.6" 6 | }, 7 | "scripts": { 8 | "build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets", 9 | "build:css": "npx @tailwindcss/cli -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify" 10 | }, 11 | "dependencies": { 12 | "@hotwired/stimulus": "^3.2.2", 13 | "@hotwired/turbo-rails": "^8.0.16", 14 | "@tailwindcss/cli": "^4.1.11", 15 | "tailwindcss": "^4.1.11" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console, 2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process), 3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view 4 | # to make the web console appear. 5 | development: 6 | adapter: async 7 | 8 | test: 9 | adapter: test 10 | 11 | production: 12 | adapter: solid_cable 13 | connects_to: 14 | database: 15 | writing: cable 16 | polling_interval: 0.1.seconds 17 | message_retention: 1.day 18 | -------------------------------------------------------------------------------- /db/cache_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema[7.2].define(version: 1) do 4 | create_table "solid_cache_entries", force: :cascade do |t| 5 | t.binary "key", limit: 1024, null: false 6 | t.binary "value", limit: 536870912, null: false 7 | t.datetime "created_at", null: false 8 | t.integer "key_hash", limit: 8, null: false 9 | t.integer "byte_size", limit: 4, null: false 10 | t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" 11 | t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" 12 | t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root "authentication#login" 3 | 4 | # Authentication routes 5 | get "login", to: "authentication#login" 6 | post "login", to: "authentication#authenticate" 7 | delete "logout", to: "authentication#logout" 8 | 9 | # Language switching route 10 | get "set_locale/:locale", to: "authentication#change_locale", as: "set_locale" 11 | 12 | # Dashboard 13 | get "dashboard", to: "dashboard#index" 14 | 15 | # Client management with RESTful routes 16 | resources :clients, only: [ :index, :new, :create, :destroy ] do 17 | collection do 18 | get :fetch_wireguard_address 19 | end 20 | end 21 | 22 | get "up" => "rails/health#show", as: :rails_health_check 23 | end 24 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by ./bin/rails stimulus:manifest:update 2 | // Run that command whenever you add a new controller or create them with 3 | // ./bin/rails generate stimulus controllerName 4 | 5 | import { application } from "./application" 6 | 7 | import ClientResultController from "./client_result_controller" 8 | application.register("client-result", ClientResultController) 9 | 10 | import DropdownController from "./dropdown_controller" 11 | application.register("dropdown", DropdownController) 12 | 13 | import FlashController from "./flash_controller" 14 | application.register("flash", FlashController) 15 | 16 | import NewClientController from "./new_client_controller" 17 | application.register("new-client", NewClientController) 18 | -------------------------------------------------------------------------------- /.devcontainer/compose.yaml: -------------------------------------------------------------------------------- 1 | name: "easy_wg_mikrotik" 2 | 3 | services: 4 | rails-app: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | 9 | volumes: 10 | - ../..:/workspaces:cached 11 | 12 | # Overrides default command so things don't shut down after the process ends. 13 | command: sleep infinity 14 | 15 | # Uncomment the next line to use a non-root user for all processes. 16 | # user: vscode 17 | 18 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 19 | # (Adding the "ports" property to this file will not forward from a Codespace.) 20 | depends_on: 21 | - selenium 22 | 23 | selenium: 24 | image: selenium/standalone-chromium 25 | restart: unless-stopped 26 | 27 | 28 | -------------------------------------------------------------------------------- /.rubocop_custom_layout_space_inside_percent_literal_brackets.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | module Cop 3 | module Layout 4 | class SpaceInsidePercentLiteralBrackets < Base 5 | MSG = 'Use spaces inside `%w[ first second ]`.' 6 | 7 | def on_array(node) 8 | return unless node.percent_literal? && node.loc.expression.source =~ /^%w\[(.+)\]$/ 9 | 10 | space_inside = node.loc.expression.source =~ /\[%s+/ 11 | add_offense(node, message: MSG) unless space_inside 12 | end 13 | 14 | def autocorrect(node) 15 | lambda do |corrector| 16 | corrector.replace(node.loc.expression, node.loc.expression.source.gsub('%w[', '%w[ ').gsub(']', ' ]')) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.0].define(version: 0) do 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. 3 | allow_browser versions: :modern 4 | 5 | before_action :set_locale 6 | 7 | private 8 | def set_locale 9 | if session[:locale].present? && I18n.available_locales.include?(session[:locale].to_sym) 10 | I18n.locale = session[:locale] 11 | else 12 | I18n.locale = I18n.default_locale 13 | end 14 | end 15 | 16 | def require_mikrotik_login 17 | unless logged_in? 18 | flash[:error] = t("flash.login_required") 19 | redirect_to login_path 20 | end 21 | end 22 | 23 | def logged_in? 24 | session[:mikrotik_host].present? && session[:mikrotik_user].present? 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /bin/kamal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'kamal' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("kamal", "kamal") 28 | -------------------------------------------------------------------------------- /app/views/shared/_flash.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% flash.each do |type, message| %> 3 |
8 |
9 | <%= message %> 10 | 15 |
16 |
17 | <% end %> 18 |
19 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Omakase Ruby styling for Rails 2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 3 | 4 | # Overwrite or add rules to create your own house style 5 | # 6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` 7 | # Layout/SpaceInsideArrayLiteralBrackets: 8 | # Enabled: false 9 | 10 | inherit_from: 11 | - ./.rubocop_bundler.yml 12 | - ./.rubocop_gemspec.yml 13 | - ./.rubocop_layout.yml 14 | - ./.rubocop_lint.yml 15 | - ./.rubocop_metrics.yml 16 | - ./.rubocop_migration.yml 17 | - ./.rubocop_naming.yml 18 | - ./.rubocop_performance.yml 19 | - ./.rubocop_rails.yml 20 | - ./.rubocop_security.yml 21 | - ./.rubocop_style.yml 22 | 23 | AllCops: 24 | Exclude: 25 | - 'test/**/*' 26 | - 'db/seeds.rb' 27 | 28 | require: 29 | - .rubocop_custom_layout_space_inside_percent_literal_brackets 30 | 31 | Layout/SpaceInsidePercentLiteralBrackets: 32 | Enabled: true 33 | -------------------------------------------------------------------------------- /app/javascript/controllers/dropdown_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["menu"] 5 | 6 | toggle() { 7 | if (this.menuTarget.classList.contains("hidden")) { 8 | this.show() 9 | } else { 10 | this.hide() 11 | } 12 | } 13 | 14 | show() { 15 | this.menuTarget.classList.remove("hidden") 16 | document.addEventListener("click", this.hideOnClickOutside.bind(this)) 17 | } 18 | 19 | hide() { 20 | this.menuTarget.classList.add("hidden") 21 | document.removeEventListener("click", this.hideOnClickOutside.bind(this)) 22 | } 23 | 24 | hideOnClickOutside(event) { 25 | if (!this.element.contains(event.target)) { 26 | this.hide() 27 | } 28 | } 29 | 30 | disconnect() { 31 | document.removeEventListener("click", this.hideOnClickOutside.bind(this)) 32 | } 33 | } -------------------------------------------------------------------------------- /app/views/pwa/service-worker.js: -------------------------------------------------------------------------------- 1 | // Add a service worker for processing Web Push notifications: 2 | // 3 | // self.addEventListener("push", async (event) => { 4 | // const { title, options } = await event.data.json() 5 | // event.waitUntil(self.registration.showNotification(title, options)) 6 | // }) 7 | // 8 | // self.addEventListener("notificationclick", function(event) { 9 | // event.notification.close() 10 | // event.waitUntil( 11 | // clients.matchAll({ type: "window" }).then((clientList) => { 12 | // for (let i = 0; i < clientList.length; i++) { 13 | // let client = clientList[i] 14 | // let clientPath = (new URL(client.url)).pathname 15 | // 16 | // if (clientPath == event.notification.data.path && "focus" in client) { 17 | // return client.focus() 18 | // } 19 | // } 20 | // 21 | // if (clients.openWindow) { 22 | // return clients.openWindow(event.notification.data.path) 23 | // } 24 | // }) 25 | // ) 26 | // }) 27 | -------------------------------------------------------------------------------- /app/javascript/controllers/flash_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["message"] 5 | static values = { 6 | autoHide: { type: Boolean, default: true }, 7 | duration: { type: Number, default: 3000 } 8 | } 9 | 10 | connect() { 11 | if (this.autoHideValue) { 12 | this.startAutoHide() 13 | } 14 | } 15 | 16 | startAutoHide() { 17 | this.timeout = setTimeout(() => { 18 | this.hide() 19 | }, this.durationValue) 20 | } 21 | 22 | hide() { 23 | this.element.style.opacity = '0' 24 | setTimeout(() => { 25 | if (this.element.parentElement) { 26 | this.element.remove() 27 | } 28 | }, 300) 29 | } 30 | 31 | close(event) { 32 | event.preventDefault() 33 | if (this.timeout) { 34 | clearTimeout(this.timeout) 35 | } 36 | this.hide() 37 | } 38 | 39 | disconnect() { 40 | if (this.timeout) { 41 | clearTimeout(this.timeout) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # Temporary files generated by your text editor or operating system 4 | # belong in git's global ignore instead: 5 | # `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore environment files but keep example file. 11 | /.env 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore pidfiles, but keep the directory. 20 | /tmp/pids/* 21 | !/tmp/pids/ 22 | !/tmp/pids/.keep 23 | 24 | # Ignore storage (uploaded files in development and any SQLite databases). 25 | /storage/* 26 | !/storage/.keep 27 | /tmp/storage/* 28 | !/tmp/storage/ 29 | !/tmp/storage/.keep 30 | 31 | /public/assets 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | /app/assets/builds/* 37 | !/app/assets/builds/.keep 38 | 39 | /node_modules 40 | 41 | /CLAUDE.md 42 | 43 | /docker-compose.override.yaml 44 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore git directory. 4 | /.git/ 5 | /.gitignore 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all default key files. 14 | /config/master.key 15 | /config/credentials/*.key 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/* 19 | /tmp/* 20 | !/log/.keep 21 | !/tmp/.keep 22 | 23 | # Ignore pidfiles, but keep the directory. 24 | /tmp/pids/* 25 | !/tmp/pids/.keep 26 | 27 | # Ignore storage (uploaded files in development and any SQLite databases). 28 | /storage/* 29 | !/storage/.keep 30 | /tmp/storage/* 31 | !/tmp/storage/.keep 32 | 33 | # Ignore assets. 34 | /node_modules/ 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | /public/assets 38 | 39 | # Ignore CI service files. 40 | /.github 41 | 42 | # Ignore Kamal files. 43 | /config/deploy*.yml 44 | /.kamal 45 | 46 | # Ignore development files 47 | /.devcontainer 48 | 49 | # Ignore Docker-related files 50 | /.dockerignore 51 | -------------------------------------------------------------------------------- /.kamal/secrets: -------------------------------------------------------------------------------- 1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, 2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either 3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. 4 | 5 | # Example of extracting secrets from 1password (or another compatible pw manager) 6 | # SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) 7 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) 8 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) 9 | 10 | # Use a GITHUB_TOKEN if private repositories are needed for the image 11 | # GITHUB_TOKEN=$(gh config get -h github.com oauth_token) 12 | 13 | # Grab the registry password from ENV 14 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD 15 | 16 | # Improve security by using a password manager. Never check config/master.key into git! 17 | RAILS_MASTER_KEY=$(cat config/master.key) 18 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # Install JavaScript dependencies 19 | system("yarn install --check-files") 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?("config/database.yml") 23 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! "bin/rails db:prepare" 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! "bin/rails log:clear tmp:clear" 31 | 32 | unless ARGV.include?("--skip-server") 33 | puts "\n== Starting development server ==" 34 | STDOUT.flush # flush the output before exec(2) so that it displays 35 | exec "bin/dev" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLES (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: storage/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: storage/test.sqlite3 22 | 23 | 24 | # Store production database in the storage/ directory, which by default 25 | # is mounted as a persistent Docker volume in config/deploy.yml. 26 | production: 27 | primary: 28 | <<: *default 29 | database: storage/production.sqlite3 30 | cache: 31 | <<: *default 32 | database: storage/production_cache.sqlite3 33 | migrations_paths: db/cache_migrate 34 | queue: 35 | <<: *default 36 | database: storage/production_queue.sqlite3 37 | migrations_paths: db/queue_migrate 38 | cable: 39 | <<: *default 40 | database: storage/production_cable.sqlite3 41 | migrations_paths: db/cable_migrate 42 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module EasyWgMikrotik 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 8.0 13 | 14 | # Please, add to the `ignore` list any other `lib` subdirectories that do 15 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 16 | # Common ones are `templates`, `generators`, or `middleware`, for example. 17 | config.autoload_lib( 18 | ignore: %w[ 19 | assets tasks 20 | ] 21 | ) 22 | 23 | # Configuration for the application, engines, and railties goes here. 24 | # 25 | # These settings can be overridden in specific environments using the files 26 | # in config/environments, which are processed later. 27 | # 28 | # config.time_zone = "Central Time (US & Canada)" 29 | # config.eager_load_paths << Rails.root.join("extras") 30 | 31 | # I18n configuration 32 | config.i18n.available_locales = [ :ko, :en, :zh, :ja ] 33 | config.i18n.default_locale = ENV.fetch("DEFAULT_LOCALE", "ko").to_sym 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLES (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://containers.dev/implementors/json_reference/. 2 | // For config options, see the README at: https://github.com/devcontainers/templates/tree/main/src/ruby 3 | { 4 | "name": "easy_wg_mikrotik", 5 | "dockerComposeFile": "compose.yaml", 6 | "service": "rails-app", 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | "features": { 11 | "ghcr.io/devcontainers/features/github-cli:1": {}, 12 | "ghcr.io/rails/devcontainer/features/activestorage": {}, 13 | "ghcr.io/devcontainers/features/node:1": {}, 14 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 15 | "ghcr.io/rails/devcontainer/features/sqlite3": {} 16 | }, 17 | 18 | "containerEnv": { 19 | "CAPYBARA_SERVER_PORT": "45678", 20 | "SELENIUM_HOST": "selenium", 21 | "KAMAL_REGISTRY_PASSWORD": "$KAMAL_REGISTRY_PASSWORD" 22 | }, 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | "forwardPorts": [3000], 26 | 27 | // Configure tool-specific properties. 28 | // "customizations": {}, 29 | 30 | // Uncomment to connect as root instead. More info: https://containers.dev/implementors/json_reference/#remoteUser. 31 | // "remoteUser": "root", 32 | 33 | 34 | // Use 'postCreateCommand' to run commands after the container is created. 35 | "postCreateCommand": "bin/setup --skip-server" 36 | } 37 | -------------------------------------------------------------------------------- /app/javascript/controllers/client_result_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | downloadConfig(event) { 5 | const clientName = event.params.clientName 6 | const config = event.params.config 7 | 8 | const blob = new Blob([config], { type: 'text/plain' }) 9 | const url = window.URL.createObjectURL(blob) 10 | const a = document.createElement('a') 11 | a.href = url 12 | a.download = `${clientName}.conf` 13 | document.body.appendChild(a) 14 | a.click() 15 | window.URL.revokeObjectURL(url) 16 | document.body.removeChild(a) 17 | } 18 | 19 | downloadQR(event) { 20 | const clientName = event.params.clientName 21 | const qrContainer = document.querySelector('#qr-code-container') 22 | const svgElement = qrContainer.querySelector('svg') 23 | 24 | if (!svgElement) { 25 | console.error('QR code SVG not found') 26 | return 27 | } 28 | 29 | const svgData = new XMLSerializer().serializeToString(svgElement) 30 | const canvas = document.createElement('canvas') 31 | const ctx = canvas.getContext('2d') 32 | const img = new Image() 33 | 34 | img.onload = function() { 35 | const padding = 20 // 40px 여백 36 | canvas.width = img.width + (padding * 2) 37 | canvas.height = img.height + (padding * 2) 38 | ctx.fillStyle = 'white' 39 | ctx.fillRect(0, 0, canvas.width, canvas.height) 40 | ctx.drawImage(img, padding, padding) 41 | 42 | canvas.toBlob(function(blob) { 43 | const url = window.URL.createObjectURL(blob) 44 | const a = document.createElement('a') 45 | a.href = url 46 | a.download = `${clientName}-qr.png` 47 | document.body.appendChild(a) 48 | a.click() 49 | window.URL.revokeObjectURL(url) 50 | document.body.removeChild(a) 51 | }) 52 | } 53 | 54 | img.src = 'data:image/svg+xml;base64,' + btoa(svgData) 55 | } 56 | } -------------------------------------------------------------------------------- /.rubocop_security.yml: -------------------------------------------------------------------------------- 1 | # Department 'Security' (7): 수정 2 | Security/CompoundHash: 3 | Description: When overwriting Object#hash to combine values, prefer delegating to 4 | Array#hash over writing a custom implementation. 5 | Enabled: true 6 | Safe: false 7 | VersionAdded: '1.28' 8 | VersionChanged: '1.51' 9 | 10 | Security/Eval: 11 | Description: The use of eval represents a serious security risk. 12 | Enabled: true 13 | VersionAdded: '0.47' 14 | 15 | # Supports --autocorrect 16 | Security/IoMethods: 17 | Description: Checks for the first argument to `IO.read`, `IO.binread`, `IO.write`, 18 | `IO.binwrite`, `IO.foreach`, and `IO.readlines`. 19 | Enabled: true 20 | Safe: false 21 | VersionAdded: '1.22' 22 | 23 | # Supports --autocorrect 24 | Security/JSONLoad: 25 | Description: Prefer usage of `JSON.parse` over `JSON.load` due to potential security 26 | issues. See reference for more information. 27 | Reference: https://ruby-doc.org/stdlib-2.7.0/libdoc/json/rdoc/JSON.html#method-i-load 28 | Enabled: true 29 | VersionAdded: '0.43' 30 | VersionChanged: '1.22' 31 | SafeAutoCorrect: false 32 | 33 | Security/MarshalLoad: 34 | Description: Avoid using of `Marshal.load` or `Marshal.restore` due to potential security 35 | issues. See reference for more information. 36 | Reference: https://ruby-doc.org/core-2.7.0/Marshal.html#module-Marshal-label-Security+considerations 37 | Enabled: true 38 | VersionAdded: '0.47' 39 | 40 | Security/Open: 41 | Description: The use of `Kernel#open` and `URI.open` represent a serious security 42 | risk. 43 | Enabled: true 44 | VersionAdded: '0.53' 45 | VersionChanged: '1.0' 46 | Safe: false 47 | 48 | # Supports --autocorrect 49 | Security/YAMLLoad: 50 | Description: Prefer usage of `YAML.safe_load` over `YAML.load` due to potential security 51 | issues. See reference for more information. 52 | Reference: https://ruby-doc.org/stdlib-2.7.0/libdoc/yaml/rdoc/YAML.html#module-YAML-label-Security 53 | Enabled: true 54 | VersionAdded: '0.47' 55 | SafeAutoCorrect: false -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. 11 | # 12 | # The ideal number of threads per worker depends both on how much time the 13 | # application spends waiting for IO operations and on how much you wish to 14 | # prioritize throughput over latency. 15 | # 16 | # As a rule of thumb, increasing the number of threads will increase how much 17 | # traffic a given process can handle (throughput), but due to CRuby's 18 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 19 | # response time (latency) of the application. 20 | # 21 | # The default is set to 3 threads as it's deemed a decent compromise between 22 | # throughput and latency for the average Rails application. 23 | # 24 | # Any libraries that use a connection pool or another resource pool should 25 | # be configured to provide at least as many connections as the number of 26 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 27 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 28 | threads threads_count, threads_count 29 | 30 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 31 | port ENV.fetch("PORT", 3000) 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | 36 | # Run the Solid Queue supervisor inside of Puma for single-server deployments 37 | plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 38 | 39 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 40 | # In other environments, only set the PID file if requested. 41 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 42 | -------------------------------------------------------------------------------- /.rubocop_bundler.yml: -------------------------------------------------------------------------------- 1 | # Department 'Bundler' (7): 수정 2 | Bundler/DuplicatedGem: 3 | Description: Checks for duplicate gem entries in Gemfile. 4 | Enabled: false 5 | Severity: warning 6 | VersionAdded: '0.46' 7 | VersionChanged: '1.40' 8 | Include: 9 | - "**/*.gemfile" 10 | - "**/Gemfile" 11 | - "**/gems.rb" 12 | 13 | Bundler/DuplicatedGroup: 14 | Description: Checks for duplicate group entries in Gemfile. 15 | Enabled: true 16 | Severity: warning 17 | VersionAdded: '1.56' 18 | Include: 19 | - "**/*.gemfile" 20 | - "**/Gemfile" 21 | - "**/gems.rb" 22 | 23 | Bundler/GemComment: 24 | Description: Add a comment describing each gem. 25 | Enabled: false 26 | VersionAdded: '0.59' 27 | VersionChanged: '0.85' 28 | Include: 29 | - "**/*.gemfile" 30 | - "**/Gemfile" 31 | - "**/gems.rb" 32 | IgnoredGems: [] 33 | OnlyFor: [] 34 | 35 | Bundler/GemFilename: 36 | Description: Enforces the filename for managing gems. 37 | Enabled: false 38 | VersionAdded: '1.20' 39 | EnforcedStyle: Gemfile 40 | SupportedStyles: 41 | - Gemfile 42 | - gems.rb 43 | Include: 44 | - "**/Gemfile" 45 | - "**/gems.rb" 46 | - "**/Gemfile.lock" 47 | - "**/gems.locked" 48 | 49 | Bundler/GemVersion: 50 | Description: Requires or forbids specifying gem versions. 51 | Enabled: false 52 | VersionAdded: '1.14' 53 | EnforcedStyle: required 54 | SupportedStyles: 55 | - required 56 | - forbidden 57 | Include: 58 | - "**/*.gemfile" 59 | - "**/Gemfile" 60 | - "**/gems.rb" 61 | AllowedGems: [] 62 | 63 | # Supports --autocorrect 64 | Bundler/InsecureProtocolSource: 65 | Description: The source `:gemcutter`, `:rubygems` and `:rubyforge` are deprecated 66 | because HTTP requests are insecure. Please change your source to 'https://rubygems.org' 67 | if possible, or 'http://rubygems.org' if not. 68 | Enabled: false 69 | Severity: warning 70 | VersionAdded: '0.50' 71 | VersionChanged: '1.40' 72 | AllowHttpProtocol: true 73 | Include: 74 | - "**/*.gemfile" 75 | - "**/Gemfile" 76 | - "**/gems.rb" 77 | 78 | # Supports --autocorrect 79 | Bundler/OrderedGems: 80 | Description: Gems within groups in the Gemfile should be alphabetically sorted. 81 | Enabled: true 82 | VersionAdded: '0.46' 83 | VersionChanged: '0.47' 84 | TreatCommentsAsGroupSeparators: true 85 | ConsiderPunctuation: false 86 | Include: 87 | - "**/*.gemfile" 88 | - "**/Gemfile" 89 | - "**/gems.rb" -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV["CI"].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } 20 | 21 | # Show full error reports. 22 | config.consider_all_requests_local = true 23 | config.cache_store = :null_store 24 | 25 | # Render exception templates for rescuable exceptions and raise for other exceptions. 26 | config.action_dispatch.show_exceptions = :rescuable 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory. 32 | config.active_storage.service = :test 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Set host to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = { host: "example.com" } 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | 51 | # Raise error when a before_action's only/except options reference missing actions. 52 | config.action_controller.raise_on_missing_callback_actions = true 53 | end 54 | -------------------------------------------------------------------------------- /app/services/mikrotik_api_service.rb: -------------------------------------------------------------------------------- 1 | require "routeros/api" 2 | 3 | # MikroTik RouterOS API 연동 서비스 4 | # WireGuard 관련 모든 MikroTik API 호출을 캡슐화하여 컨트롤러에서 분리 5 | # 세션 기반 인증 정보를 사용하여 RouterOS와 안전한 통신 제공 6 | class MikrotikApiService 7 | # 세션 정보를 받아서 서비스 인스턴스 초기화 8 | # 세션에는 MikroTik 연결에 필요한 모든 정보가 포함됨 9 | def initialize(session) 10 | @session = session 11 | end 12 | 13 | # RouterOS API 연결 생성 및 인증 14 | # 세션에 저장된 연결 정보를 사용하여 MikroTik 라우터에 로그인 15 | # 성공시 API 객체 반환, 실패시 nil 반환 16 | def connect 17 | return nil unless authenticated? 18 | 19 | # RouterOS API 객체 생성 (호스트, 포트, SSL 설정) 20 | api = RouterOS::API.new( 21 | @session[:mikrotik_host], 22 | @session[:mikrotik_port].to_i, 23 | ssl: @session[:mikrotik_ssl] 24 | ) 25 | 26 | # 사용자 인증 시도 27 | if api.login(@session[:mikrotik_user], @session[:mikrotik_password]).ok? 28 | api 29 | else 30 | nil 31 | end 32 | end 33 | 34 | # MikroTik에서 활성화된 WireGuard 인터페이스 목록 조회 35 | # 비활성화된 인터페이스는 자동으로 제외하여 사용 가능한 인터페이스만 반환 36 | def fetch_wireguard_interfaces(api) 37 | response = api.command("/interface/wireguard/print") 38 | if response.ok? 39 | # 인터페이스 정보를 해시 형태로 변환하고 비활성화된 것 제외 40 | response.data.map do |iface| 41 | { 42 | name: iface[:name], 43 | public_key: iface[:"public-key"], 44 | listen_port: iface[:"listen-port"], 45 | disabled: iface[:disabled] == "true" 46 | } 47 | end.reject { |iface| iface[:disabled] } 48 | else 49 | [] 50 | end 51 | end 52 | 53 | # 지정된 WireGuard 인터페이스의 서버 공개키 조회 54 | # 클라이언트 설정 파일 생성시 필요한 서버측 공개키 정보 추출 55 | def fetch_server_public_key(api, interface_name) 56 | response = api.command("/interface/wireguard/print") 57 | response.data.each do |iface| 58 | return iface[:"public-key"] if iface[:name] == interface_name 59 | end 60 | nil 61 | end 62 | 63 | # MikroTik에 새로운 WireGuard 피어 등록 64 | # 클라이언트 정보를 RouterOS에 추가하여 VPN 연결 허용 65 | # 피어 이름, 인터페이스, 공개키, 허용 주소, keepalive 설정 포함 66 | def register_peer(api, public_key, client_address, client_name, interface_name, persistent_keepalive) 67 | api.command( 68 | "/interface/wireguard/peers/add", 69 | { 70 | "name" => client_name, # 피어 식별 이름 71 | "interface" => interface_name, # 연결할 WireGuard 인터페이스 72 | "public-key" => public_key, # 클라이언트 공개키 73 | "allowed-address" => client_address, # 클라이언트에게 할당된 IP 주소 74 | "persistent-keepalive" => persistent_keepalive.to_s # 연결 유지 주기 (초) 75 | } 76 | ) 77 | end 78 | 79 | private 80 | # 세션에 MikroTik 인증 정보가 올바르게 저장되어 있는지 확인 81 | # 최소한 호스트와 사용자명이 있어야 API 연결 시도 가능 82 | def authenticated? 83 | @session[:mikrotik_host].present? && @session[:mikrotik_user].present? 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 4 | gem "rails", "~> 8.0.2" 5 | # The modern asset pipeline for Rails [https://github.com/rails/propshaft] 6 | gem "propshaft" 7 | # Use sqlite3 as the database for Active Record 8 | gem "sqlite3", ">= 2.1" 9 | # Use the Puma web server [https://github.com/puma/puma] 10 | gem "puma", ">= 5.0" 11 | # Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails] 12 | gem "jsbundling-rails" 13 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 14 | gem "turbo-rails" 15 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 16 | gem "stimulus-rails" 17 | # Bundle and process CSS [https://github.com/rails/cssbundling-rails] 18 | gem "cssbundling-rails" 19 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 20 | gem "jbuilder" 21 | 22 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 23 | # gem "bcrypt", "~> 3.1.7" 24 | 25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 26 | gem "tzinfo-data", platforms: %i[ windows jruby ] 27 | 28 | # Use the database-backed adapters for Rails.cache, Active Job, and Action Cable 29 | gem "solid_cable" 30 | gem "solid_cache" 31 | gem "solid_queue" 32 | 33 | # Reduces boot times through caching; required in config/boot.rb 34 | gem "bootsnap", require: false 35 | 36 | # Deploy this application anywhere as a Docker container [https://kamal-deploy.org] 37 | gem "kamal", require: false 38 | 39 | # Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] 40 | gem "thruster", require: false 41 | 42 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 43 | # gem "image_processing", "~> 1.2" 44 | 45 | group :development, :test do 46 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 47 | gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" 48 | 49 | # Static analysis for security vulnerabilities [https://brakemanscanner.org/] 50 | gem "brakeman", "~> 7.1.0", require: true 51 | 52 | # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] 53 | gem "rubocop-rails-omakase", require: false 54 | 55 | gem "erb_lint", require: false 56 | end 57 | 58 | group :development do 59 | # Use console on exceptions pages [https://github.com/rails/web-console] 60 | gem "web-console" 61 | end 62 | 63 | group :test do 64 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 65 | gem "capybara" 66 | gem "selenium-webdriver" 67 | end 68 | 69 | gem "base64" 70 | gem "dotenv-rails" 71 | gem "rails-i18n", "~> 8.0.0" 72 | gem "rbnacl" 73 | gem "routeros-api" 74 | gem "rqrcode" 75 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } 24 | else 25 | config.action_controller.perform_caching = false 26 | end 27 | 28 | # Change to :null_store to avoid any caching. 29 | config.cache_store = :memory_store 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | # Make template changes take effect immediately. 38 | config.action_mailer.perform_caching = false 39 | 40 | # Set localhost to be used by links generated in mailer templates. 41 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 } 42 | 43 | # Print deprecation notices to the Rails logger. 44 | config.active_support.deprecation = :log 45 | 46 | # Raise an error on page load if there are pending migrations. 47 | config.active_record.migration_error = :page_load 48 | 49 | # Highlight code that triggered database queries in logs. 50 | config.active_record.verbose_query_logs = true 51 | 52 | # Append comments with runtime information tags to SQL queries in logs. 53 | config.active_record.query_log_tags_enabled = true 54 | 55 | # Highlight code that enqueued background job in logs. 56 | config.active_job.verbose_enqueue_logs = true 57 | 58 | # Raises error for missing translations. 59 | # config.i18n.raise_on_missing_translations = true 60 | 61 | # Annotate rendered view with file names. 62 | config.action_view.annotate_rendered_view_with_filenames = true 63 | 64 | # Uncomment if you wish to allow Action Cable access from any origin. 65 | # config.action_cable.disable_request_forgery_protection = true 66 | 67 | # Raise error when a before_action's only/except options reference missing actions. 68 | config.action_controller.raise_on_missing_callback_actions = true 69 | 70 | # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. 71 | # config.generators.apply_rubocop_autocorrect_after_generate! 72 | end 73 | -------------------------------------------------------------------------------- /app/javascript/controllers/new_client_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["interfaceSelect", "interfaceInfo", "interfacePublicKey", "interfaceListenPort"] 5 | static values = { 6 | wireguardInterfaces: Array, 7 | serverAddress: String 8 | } 9 | 10 | connect() { 11 | // 페이지 로드시 첫 번째 인터페이스 선택 12 | if (this.hasInterfaceSelectTarget && this.interfaceSelectTarget.value) { 13 | this.updateInterfaceInfo() 14 | } 15 | } 16 | 17 | updateInterfaceInfo() { 18 | if (!this.hasInterfaceSelectTarget) { 19 | return 20 | } 21 | 22 | const interfaceName = this.interfaceSelectTarget.value 23 | 24 | if (!interfaceName) { 25 | if (this.hasInterfaceInfoTarget) { 26 | this.interfaceInfoTarget.classList.add('hidden') 27 | } 28 | return 29 | } 30 | 31 | const selectedInterface = this.wireguardInterfacesValue.find(iface => iface.name === interfaceName) 32 | 33 | if (selectedInterface && this.hasInterfaceInfoTarget) { 34 | if (this.hasInterfacePublicKeyTarget) { 35 | this.interfacePublicKeyTarget.textContent = selectedInterface.public_key || '없음' 36 | } 37 | if (this.hasInterfaceListenPortTarget) { 38 | this.interfaceListenPortTarget.textContent = selectedInterface.listen_port || '설정되지 않음' 39 | } 40 | 41 | const subnetInput = this.element.querySelector("input[name='client[subnet_prefix]'], input[name='subnet_prefix']") 42 | const allowedIpsInput = this.element.querySelector("input[name='client[allowed_ips]'], input[name='allowed_ips']") 43 | 44 | if (subnetInput) subnetInput.value = "" 45 | if (allowedIpsInput) allowedIpsInput.value = "" 46 | 47 | fetch(`/clients/fetch_wireguard_address?interface=${encodeURIComponent(interfaceName)}`) 48 | .then(response => { 49 | if (!response.ok) throw new Error("API 호출 실패") 50 | return response.json() 51 | }) 52 | .then(data => { 53 | if (data.network) { 54 | if (subnetInput) subnetInput.value = data.network 55 | if (allowedIpsInput) allowedIpsInput.value = `${data.network}/24` 56 | if (data.bridge_network) { 57 | if (allowedIpsInput) allowedIpsInput.value += `,${data.bridge_network}/24` 58 | } 59 | } 60 | }) 61 | .catch(error => { 62 | // 네트워크 오류시 무시 (사용자에게 별도 알림 불필요) 63 | }) 64 | 65 | const endpointInput = this.element.querySelector("input[name='client[endpoint]'], input[name='endpoint']") 66 | if (endpointInput && selectedInterface.listen_port) { 67 | endpointInput.value = `${this.serverAddressValue}:${selectedInterface.listen_port}` 68 | } 69 | 70 | this.interfaceInfoTarget.classList.remove('hidden') 71 | } else if (this.hasInterfaceInfoTarget) { 72 | this.interfaceInfoTarget.classList.add('hidden') 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /.rubocop_metrics.yml: -------------------------------------------------------------------------------- 1 | # Department 'Metrics' (10): 미수정 2 | Metrics/AbcSize: 3 | Description: A calculated magnitude based on number of assignments, branches, and 4 | conditions. 5 | Reference: 6 | - http://c2.com/cgi/wiki?AbcMetric 7 | - https://en.wikipedia.org/wiki/ABC_Software_Metric 8 | Enabled: false 9 | VersionAdded: '0.27' 10 | VersionChanged: '1.5' 11 | AllowedMethods: [] 12 | AllowedPatterns: [] 13 | CountRepeatedAttributes: true 14 | Max: 17 15 | 16 | Metrics/BlockLength: 17 | Description: Avoid long blocks with many lines. 18 | Enabled: false 19 | VersionAdded: '0.44' 20 | VersionChanged: '1.5' 21 | CountComments: false 22 | Max: 25 23 | CountAsOne: [] 24 | AllowedMethods: 25 | - refine 26 | AllowedPatterns: [] 27 | Exclude: 28 | - "/Users/rubyon/Desktop/liaf-rails/**/*.gemspec" 29 | 30 | Metrics/BlockNesting: 31 | Description: Avoid excessive block nesting. 32 | StyleGuide: "#three-is-the-number-thou-shalt-count" 33 | Enabled: false 34 | VersionAdded: '0.25' 35 | VersionChanged: '1.65' 36 | CountBlocks: false 37 | CountModifierForms: false 38 | Max: 3 39 | 40 | Metrics/ClassLength: 41 | Description: Avoid classes longer than 100 lines of code. 42 | Enabled: false 43 | VersionAdded: '0.25' 44 | VersionChanged: '0.87' 45 | CountComments: false 46 | Max: 100 47 | CountAsOne: [] 48 | 49 | Metrics/CollectionLiteralLength: 50 | Description: Checks for `Array` or `Hash` literals with many entries. 51 | Enabled: false 52 | VersionAdded: '1.47' 53 | LengthThreshold: 250 54 | 55 | Metrics/CyclomaticComplexity: 56 | Description: A complexity metric that is strongly correlated to the number of test 57 | cases needed to validate a method. 58 | Enabled: false 59 | VersionAdded: '0.25' 60 | VersionChanged: '0.81' 61 | AllowedMethods: [] 62 | AllowedPatterns: [] 63 | Max: 7 64 | 65 | Metrics/MethodLength: 66 | Description: Avoid methods longer than 10 lines of code. 67 | StyleGuide: "#short-methods" 68 | Enabled: false 69 | VersionAdded: '0.25' 70 | VersionChanged: '1.5' 71 | CountComments: false 72 | Max: 10 73 | CountAsOne: [] 74 | AllowedMethods: [] 75 | AllowedPatterns: [] 76 | 77 | Metrics/ModuleLength: 78 | Description: Avoid modules longer than 100 lines of code. 79 | Enabled: false 80 | VersionAdded: '0.31' 81 | VersionChanged: '0.87' 82 | CountComments: false 83 | Max: 100 84 | CountAsOne: [] 85 | 86 | Metrics/ParameterLists: 87 | Description: Avoid parameter lists longer than three or four parameters. 88 | StyleGuide: "#too-many-params" 89 | Enabled: false 90 | VersionAdded: '0.25' 91 | VersionChanged: '1.5' 92 | Max: 5 93 | CountKeywordArgs: true 94 | MaxOptionalParameters: 3 95 | 96 | Metrics/PerceivedComplexity: 97 | Description: A complexity metric geared towards measuring complexity for a human reader. 98 | Enabled: false 99 | VersionAdded: '0.25' 100 | VersionChanged: '0.81' 101 | AllowedMethods: [] 102 | AllowedPatterns: [] 103 | Max: 8 -------------------------------------------------------------------------------- /.rubocop_gemspec.yml: -------------------------------------------------------------------------------- 1 | # Department 'Gemspec' (9): 미수정 2 | # Supports --autocorrect 3 | Gemspec/AddRuntimeDependency: 4 | Description: Prefer `add_dependency` over `add_runtime_dependency`. 5 | StyleGuide: "#add_dependency_vs_add_runtime_dependency" 6 | Reference: https://github.com/rubygems/rubygems/issues/7799#issuecomment-2192720316 7 | Enabled: false 8 | VersionAdded: '1.65' 9 | Include: 10 | - "**/*.gemspec" 11 | 12 | Gemspec/DependencyVersion: 13 | Description: Requires or forbids specifying gem dependency versions. 14 | Enabled: false 15 | VersionAdded: '1.29' 16 | EnforcedStyle: required 17 | SupportedStyles: 18 | - required 19 | - forbidden 20 | Include: 21 | - "**/*.gemspec" 22 | AllowedGems: [] 23 | 24 | # Supports --autocorrect 25 | Gemspec/DeprecatedAttributeAssignment: 26 | Description: Checks that deprecated attribute assignments are not set in a gemspec 27 | file. 28 | Enabled: false 29 | Severity: warning 30 | VersionAdded: '1.30' 31 | VersionChanged: '1.40' 32 | Include: 33 | - "**/*.gemspec" 34 | 35 | Gemspec/DevelopmentDependencies: 36 | Description: Checks that development dependencies are specified in Gemfile rather 37 | than gemspec. 38 | Enabled: false 39 | VersionAdded: '1.44' 40 | EnforcedStyle: Gemfile 41 | SupportedStyles: 42 | - Gemfile 43 | - gems.rb 44 | - gemspec 45 | AllowedGems: [] 46 | Include: 47 | - "**/*.gemspec" 48 | - "**/Gemfile" 49 | - "**/gems.rb" 50 | 51 | Gemspec/DuplicatedAssignment: 52 | Description: An attribute assignment method calls should be listed only once in a 53 | gemspec. 54 | Enabled: false 55 | Severity: warning 56 | VersionAdded: '0.52' 57 | VersionChanged: '1.40' 58 | Include: 59 | - "**/*.gemspec" 60 | 61 | # Supports --autocorrect 62 | Gemspec/OrderedDependencies: 63 | Description: Dependencies in the gemspec should be alphabetically sorted. 64 | Enabled: false 65 | VersionAdded: '0.51' 66 | TreatCommentsAsGroupSeparators: true 67 | ConsiderPunctuation: false 68 | Include: 69 | - "**/*.gemspec" 70 | 71 | # Supports --autocorrect 72 | Gemspec/RequireMFA: 73 | Description: Checks that the gemspec has metadata to require Multi-Factor Authentication 74 | from RubyGems. 75 | Enabled: false 76 | Severity: warning 77 | VersionAdded: '1.23' 78 | VersionChanged: '1.40' 79 | Reference: 80 | - https://guides.rubygems.org/mfa-requirement-opt-in/ 81 | Include: 82 | - "**/*.gemspec" 83 | 84 | Gemspec/RequiredRubyVersion: 85 | Description: Checks that `required_ruby_version` of gemspec is specified and equal 86 | to `TargetRubyVersion` of .rubocop.yml. 87 | Enabled: false 88 | Severity: warning 89 | VersionAdded: '0.52' 90 | VersionChanged: '1.40' 91 | Include: 92 | - "**/*.gemspec" 93 | 94 | Gemspec/RubyVersionGlobalsUsage: 95 | Description: Checks usage of RUBY_VERSION in gemspec. 96 | StyleGuide: "#no-ruby-version-in-the-gemspec" 97 | Enabled: false 98 | Severity: warning 99 | VersionAdded: '0.72' 100 | VersionChanged: '1.40' 101 | Include: 102 | - "**/*.gemspec" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: 5 | # docker build -t easy_wg_mikrotik . 6 | # docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name easy_wg_mikrotik easy_wg_mikrotik 7 | 8 | # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html 9 | 10 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version 11 | ARG RUBY_VERSION=3.4.2 12 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base 13 | 14 | # Rails app lives here 15 | WORKDIR /rails 16 | 17 | # Install base packages 18 | RUN apt-get update -qq && \ 19 | apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libsodium-dev && \ 20 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 21 | 22 | # Set production environment 23 | ENV RAILS_ENV="production" \ 24 | BUNDLE_DEPLOYMENT="1" \ 25 | BUNDLE_PATH="/usr/local/bundle" \ 26 | BUNDLE_WITHOUT="development" 27 | 28 | # Throw-away build stage to reduce size of final image 29 | FROM base AS build 30 | 31 | # Install packages needed to build gems and node modules 32 | RUN apt-get update -qq && \ 33 | apt-get install --no-install-recommends -y build-essential git libyaml-dev node-gyp pkg-config python-is-python3 && \ 34 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 35 | 36 | # Install JavaScript dependencies 37 | ARG NODE_VERSION=23.10.0 38 | ARG YARN_VERSION=1.22.22 39 | ENV PATH=/usr/local/node/bin:$PATH 40 | RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ 41 | /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ 42 | npm install -g yarn@$YARN_VERSION && \ 43 | rm -rf /tmp/node-build-master 44 | 45 | # Install application gems 46 | COPY Gemfile Gemfile.lock ./ 47 | RUN bundle install && \ 48 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ 49 | bundle exec bootsnap precompile --gemfile 50 | 51 | # Install node modules 52 | COPY package.json yarn.lock ./ 53 | RUN yarn install --immutable 54 | 55 | # Copy application code 56 | COPY . . 57 | 58 | # Precompile bootsnap code for faster boot times 59 | RUN bundle exec bootsnap precompile app/ lib/ 60 | 61 | # Precompiling assets for production without requiring secret RAILS_MASTER_KEY 62 | RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile 63 | 64 | 65 | RUN rm -rf node_modules 66 | 67 | 68 | # Final stage for app image 69 | FROM base 70 | 71 | # Copy built artifacts: gems, application 72 | COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" 73 | COPY --from=build /rails /rails 74 | 75 | # Run and own only the runtime files as a non-root user for security 76 | RUN groupadd --system --gid 1000 rails && \ 77 | useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ 78 | chown -R rails:rails db log storage tmp 79 | USER 1000:1000 80 | 81 | # Entrypoint prepares the database. 82 | ENTRYPOINT ["/rails/bin/docker-entrypoint"] 83 | 84 | # Start server via Thruster by default, this can be overwritten at runtime 85 | EXPOSE 3000 86 | CMD ["./bin/thrust", "./bin/rails", "server", "-b", "0.0.0.0"] 87 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../Gemfile", __dir__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || 66 | cli_arg_version || 67 | bundler_requirement_for(lockfile_version) 68 | end 69 | 70 | def bundler_requirement_for(version) 71 | return "#{Gem::Requirement.default}.a" unless version 72 | 73 | bundler_gem_version = Gem::Version.new(version) 74 | 75 | bundler_gem_version.approximate_recommendation 76 | end 77 | 78 | def load_bundler! 79 | ENV["BUNDLE_GEMFILE"] ||= gemfile 80 | 81 | activate_bundler 82 | end 83 | 84 | def activate_bundler 85 | gem_error = activation_error_handling do 86 | gem "bundler", bundler_requirement 87 | end 88 | return if gem_error.nil? 89 | require_error = activation_error_handling do 90 | require "bundler/version" 91 | end 92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 93 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 94 | exit 42 95 | end 96 | 97 | def activation_error_handling 98 | yield 99 | nil 100 | rescue StandardError, LoadError => e 101 | e 102 | end 103 | end 104 | 105 | m.load_bundler! 106 | 107 | if m.invoked_as_script? 108 | load Gem.bin_path("bundler", "bundle") 109 | end 110 | -------------------------------------------------------------------------------- /app/controllers/authentication_controller.rb: -------------------------------------------------------------------------------- 1 | require "routeros/api" 2 | 3 | # MikroTik 라우터 인증 및 세션 관리 컨트롤러 4 | # RouterOS API를 사용한 로그인/로그아웃과 다국어 지원을 담당 5 | class AuthenticationController < ApplicationController 6 | # 로그인 폼 페이지 표시 7 | # 이미 로그인된 상태면 대시보드로 리다이렉트 8 | def login 9 | redirect_to dashboard_path if logged_in? 10 | end 11 | 12 | # MikroTik 라우터 인증 처리 13 | # 사용자 입력 정보로 RouterOS API 연결 시도하고 세션에 저장 14 | def authenticate 15 | # 폼에서 전달받은 연결 정보 추출, 환경 변수를 기본값으로 사용 16 | host = params[:mikrotik_host].presence || ENV.fetch("MIKROTIK_HOST", "192.168.1.1") 17 | user = params[:mikrotik_user].presence 18 | password = params[:mikrotik_password].presence 19 | port = params[:mikrotik_port].presence || ENV.fetch("MIKROTIK_PORT", "8728") 20 | ssl = false # 현재 SSL 연결 비활성화 21 | remember_me = params[:remember_me] == "1" 22 | 23 | # 필수 필드 검증 - 호스트, 사용자명, 비밀번호는 반드시 필요 24 | if host.blank? || user.blank? || password.blank? 25 | flash.now[:error] = t("flash.required_fields") 26 | render :login and return 27 | end 28 | 29 | begin 30 | # RouterOS API 객체 생성 및 로그인 시도 31 | api = RouterOS::API.new(host, port.to_i, ssl: ssl) 32 | login_response = api.login(user, password) 33 | 34 | if login_response.ok? 35 | # 로그인 성공시 세션에 연결 정보 저장 36 | # 추후 MikroTik API 호출시 재사용됨 37 | session[:mikrotik_host] = host 38 | session[:mikrotik_user] = user 39 | session[:mikrotik_password] = password 40 | session[:mikrotik_port] = port 41 | session[:mikrotik_ssl] = ssl 42 | 43 | # 로그인 정보 기억하기 처리 44 | if remember_me 45 | # 30일간 쿠키에 기본 연결 정보 저장 (비밀번호 제외) 46 | # Rails가 자동으로 암호화하므로 보안상 안전함 47 | cookies.permanent[:remember_mikrotik_login] = { 48 | host: host, 49 | user: user, 50 | port: port 51 | }.to_json 52 | else 53 | # 체크박스 해제시 기존 저장된 쿠키 삭제 54 | cookies.delete(:remember_mikrotik_login) 55 | end 56 | 57 | api.close 58 | flash[:success] = t("flash.login_success") 59 | redirect_to dashboard_path 60 | else 61 | # 로그인 실패시 에러 메시지 표시 62 | flash.now[:error] = t("flash.login_failed") 63 | render :login 64 | end 65 | rescue => e 66 | # 네트워크 연결 실패 등 예외 처리 67 | flash.now[:error] = t("flash.connection_failed", error: e.message) 68 | render :login 69 | end 70 | end 71 | 72 | # 로그아웃 처리 73 | # 세션에서 모든 MikroTik 연결 정보 삭제 74 | def logout 75 | session.delete(:mikrotik_host) 76 | session.delete(:mikrotik_user) 77 | session.delete(:mikrotik_password) 78 | session.delete(:mikrotik_port) 79 | session.delete(:mikrotik_ssl) 80 | flash[:success] = t("flash.logout_success") 81 | redirect_to login_path 82 | end 83 | 84 | # 애플리케이션 언어 변경 처리 85 | # 지원 언어: 한국어, 영어, 중국어, 일본어 86 | def change_locale 87 | locale = params[:locale] 88 | # 유효한 로케일인지 확인 후 세션에 저장 89 | if locale.present? && I18n.available_locales.include?(locale.to_sym) 90 | session[:locale] = locale 91 | flash[:success] = t("flash.language_changed") 92 | end 93 | 94 | # 현재 로그인 상태에 따라 적절한 페이지로 리다이렉트 95 | if logged_in? 96 | redirect_to dashboard_path 97 | else 98 | redirect_to login_path 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-deploy hook 4 | # 5 | # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. 6 | # 7 | # Fails unless the combined status is "success" 8 | # 9 | # These environment variables are available: 10 | # KAMAL_RECORDED_AT 11 | # KAMAL_PERFORMER 12 | # KAMAL_VERSION 13 | # KAMAL_HOSTS 14 | # KAMAL_COMMAND 15 | # KAMAL_SUBCOMMAND 16 | # KAMAL_ROLES (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | # Only check the build status for production deployments 20 | if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" 21 | exit 0 22 | end 23 | 24 | require "bundler/inline" 25 | 26 | # true = install gems so this is fast on repeat invocations 27 | gemfile(true, quiet: true) do 28 | source "https://rubygems.org" 29 | 30 | gem "octokit" 31 | gem "faraday-retry" 32 | end 33 | 34 | MAX_ATTEMPTS = 72 35 | ATTEMPTS_GAP = 10 36 | 37 | def exit_with_error(message) 38 | $stderr.puts message 39 | exit 1 40 | end 41 | 42 | class GithubStatusChecks 43 | attr_reader :remote_url, :git_sha, :github_client, :combined_status 44 | 45 | def initialize 46 | @remote_url = github_repo_from_remote_url 47 | @git_sha = `git rev-parse HEAD`.strip 48 | @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) 49 | refresh! 50 | end 51 | 52 | def refresh! 53 | @combined_status = github_client.combined_status(remote_url, git_sha) 54 | end 55 | 56 | def state 57 | combined_status[:state] 58 | end 59 | 60 | def first_status_url 61 | first_status = combined_status[:statuses].find { |status| status[:state] == state } 62 | first_status && first_status[:target_url] 63 | end 64 | 65 | def complete_count 66 | combined_status[:statuses].count { |status| status[:state] != "pending"} 67 | end 68 | 69 | def total_count 70 | combined_status[:statuses].count 71 | end 72 | 73 | def current_status 74 | if total_count > 0 75 | "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." 76 | else 77 | "Build not started..." 78 | end 79 | end 80 | 81 | private 82 | def github_repo_from_remote_url 83 | url = `git config --get remote.origin.url`.strip.delete_suffix(".git") 84 | if url.start_with?("https://github.com/") 85 | url.delete_prefix("https://github.com/") 86 | elsif url.start_with?("git@github.com:") 87 | url.delete_prefix("git@github.com:") 88 | else 89 | url 90 | end 91 | end 92 | end 93 | 94 | 95 | $stdout.sync = true 96 | 97 | begin 98 | puts "Checking build status..." 99 | 100 | attempts = 0 101 | checks = GithubStatusChecks.new 102 | 103 | loop do 104 | case checks.state 105 | when "success" 106 | puts "Checks passed, see #{checks.first_status_url}" 107 | exit 0 108 | when "failure" 109 | exit_with_error "Checks failed, see #{checks.first_status_url}" 110 | when "pending" 111 | attempts += 1 112 | end 113 | 114 | exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS 115 | 116 | puts checks.current_status 117 | sleep(ATTEMPTS_GAP) 118 | checks.refresh! 119 | end 120 | rescue Octokit::NotFound 121 | exit_with_error "Build status could not be found" 122 | end 123 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ main ] 7 | 8 | jobs: 9 | scan_ruby: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: .ruby-version 20 | bundler-cache: true 21 | 22 | - name: Scan for common Rails security vulnerabilities using static analysis 23 | run: bin/brakeman --no-pager 24 | 25 | lint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: .ruby-version 35 | bundler-cache: true 36 | 37 | - name: Lint code for consistent style 38 | run: bin/rubocop -f github 39 | 40 | test: 41 | runs-on: ubuntu-latest 42 | 43 | # services: 44 | # redis: 45 | # image: redis 46 | # ports: 47 | # - 6379:6379 48 | # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 49 | steps: 50 | - name: Install packages 51 | run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev node-gyp pkg-config python-is-python3 google-chrome-stable 52 | 53 | - name: Checkout code 54 | uses: actions/checkout@v4 55 | 56 | - name: Set up Ruby 57 | uses: ruby/setup-ruby@v1 58 | with: 59 | ruby-version: .ruby-version 60 | bundler-cache: true 61 | 62 | - name: Run tests 63 | env: 64 | RAILS_ENV: test 65 | # REDIS_URL: redis://localhost:6379/0 66 | run: bin/rails db:test:prepare test test:system 67 | 68 | - name: Keep screenshots from failed system tests 69 | uses: actions/upload-artifact@v4 70 | if: failure() 71 | with: 72 | name: screenshots 73 | path: ${{ github.workspace }}/tmp/screenshots 74 | if-no-files-found: ignore 75 | 76 | build: 77 | runs-on: ubuntu-latest 78 | needs: test 79 | 80 | steps: 81 | - name: Install packages 82 | run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev node-gyp pkg-config python-is-python3 google-chrome-stable 83 | 84 | - name: Checkout code 85 | uses: actions/checkout@v4 86 | 87 | - name: Set up Ruby 88 | uses: ruby/setup-ruby@v1 89 | with: 90 | ruby-version: .ruby-version 91 | bundler-cache: true 92 | 93 | - name: Set version 94 | run: | 95 | VERSION=$(TZ=Asia/Seoul date +'%Y%m%d%H%M%S') 96 | echo "VERSION=${VERSION}" >> $GITHUB_ENV 97 | 98 | - name: Set up Docker Buildx 99 | uses: docker/setup-buildx-action@v3 100 | with: 101 | driver: docker-container 102 | 103 | - name: Login to Docker Hub 104 | uses: docker/login-action@v3 105 | with: 106 | username: ${{ vars.DOCKER_USERNAME }} 107 | password: ${{ secrets.DOCKER_PASSWORD }} 108 | 109 | - name: Set up QEMU 110 | uses: docker/setup-qemu-action@v3 111 | 112 | - name: Set up Docker Buildx 113 | uses: docker/setup-buildx-action@v3 114 | 115 | - name: Build and Push 116 | uses: docker/build-push-action@v6 117 | with: 118 | context: . 119 | push: true 120 | platforms: linux/amd64 121 | tags: | 122 | ${{ vars.DOCKER_USERNAME }}/easy_wg_mikrotik:${{ env.VERSION }} 123 | ${{ vars.DOCKER_USERNAME }}/easy_wg_mikrotik:latest 124 | cache-from: type=gha 125 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). 10 | config.eager_load = true 11 | 12 | # Full error reports are disabled. 13 | config.consider_all_requests_local = false 14 | 15 | # Turn on fragment caching in view templates. 16 | config.action_controller.perform_caching = true 17 | 18 | # Cache assets for far-future expiry since they are all digest stamped. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } 20 | 21 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 22 | # config.asset_host = "http://assets.example.com" 23 | 24 | # Store uploaded files on the local file system (see config/storage.yml for options). 25 | config.active_storage.service = :local 26 | 27 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 28 | config.assume_ssl = true 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | config.force_ssl = true 32 | 33 | # Skip http-to-https redirect for the default health check endpoint. 34 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 35 | 36 | # Log to STDOUT with the current request id as a default log tag. 37 | config.log_tags = [ :request_id ] 38 | config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) 39 | 40 | # Change to "debug" to log everything (including potentially personally-identifiable information!) 41 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 42 | 43 | # Prevent health checks from clogging up the logs. 44 | config.silence_healthcheck_path = "/up" 45 | 46 | # Don't log any deprecations. 47 | config.active_support.report_deprecations = false 48 | 49 | # Replace the default in-process memory cache store with a durable alternative. 50 | config.cache_store = :solid_cache_store 51 | 52 | # Replace the default in-process and non-durable queuing backend for Active Job. 53 | config.active_job.queue_adapter = :solid_queue 54 | config.solid_queue.connects_to = { database: { writing: :queue } } 55 | 56 | # Ignore bad email addresses and do not raise email delivery errors. 57 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 58 | # config.action_mailer.raise_delivery_errors = false 59 | 60 | # Set host to be used by links generated in mailer templates. 61 | config.action_mailer.default_url_options = { host: "example.com" } 62 | 63 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. 64 | # config.action_mailer.smtp_settings = { 65 | # user_name: Rails.application.credentials.dig(:smtp, :user_name), 66 | # password: Rails.application.credentials.dig(:smtp, :password), 67 | # address: "smtp.example.com", 68 | # port: 587, 69 | # authentication: :plain 70 | # } 71 | 72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 73 | # the I18n.default_locale when a translation cannot be found). 74 | config.i18n.fallbacks = true 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | 79 | # Only use :id for inspections in production. 80 | config.active_record.attributes_for_inspect = [ :id ] 81 | 82 | # Enable DNS rebinding protection and other `Host` header attacks. 83 | # config.hosts = [ 84 | # "example.com", # Allow requests from example.com 85 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 86 | # ] 87 | # 88 | # Skip DNS rebinding protection for the default health check endpoint. 89 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 90 | end 91 | -------------------------------------------------------------------------------- /config/deploy.yml: -------------------------------------------------------------------------------- 1 | # Name of your application. Used to uniquely configure containers. 2 | service: easy_wg_mikrotik 3 | 4 | # Name of the container image. 5 | image: your-user/easy_wg_mikrotik 6 | 7 | # Deploy to these servers. 8 | servers: 9 | web: 10 | - 192.168.0.1 11 | # job: 12 | # hosts: 13 | # - 192.168.0.1 14 | # cmd: bin/jobs 15 | 16 | # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. 17 | # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. 18 | # 19 | # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. 20 | proxy: 21 | ssl: true 22 | host: app.example.com 23 | 24 | # Credentials for your image host. 25 | registry: 26 | # Specify the registry server, if you're not using Docker Hub 27 | # server: registry.digitalocean.com / ghcr.io / ... 28 | username: your-user 29 | 30 | # Always use an access token rather than real password when possible. 31 | password: 32 | - KAMAL_REGISTRY_PASSWORD 33 | 34 | # Inject ENV variables into containers (secrets come from .kamal/secrets). 35 | env: 36 | secret: 37 | - RAILS_MASTER_KEY 38 | clear: 39 | # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. 40 | # When you start using multiple servers, you should split out job processing to a dedicated machine. 41 | SOLID_QUEUE_IN_PUMA: true 42 | 43 | # Set number of processes dedicated to Solid Queue (default: 1) 44 | # JOB_CONCURRENCY: 3 45 | 46 | # Set number of cores available to the application on each server (default: 1). 47 | # WEB_CONCURRENCY: 2 48 | 49 | # Match this to any external database server to configure Active Record correctly 50 | # Use easy_wg_mikrotik-db for a db accessory server on same machine via local kamal docker network. 51 | # DB_HOST: 192.168.0.2 52 | 53 | # Log everything from Rails 54 | # RAILS_LOG_LEVEL: debug 55 | 56 | # Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: 57 | # "bin/kamal logs -r job" will tail logs from the first server in the job section. 58 | aliases: 59 | console: app exec --interactive --reuse "bin/rails console" 60 | shell: app exec --interactive --reuse "bash" 61 | logs: app logs -f 62 | dbc: app exec --interactive --reuse "bin/rails dbconsole" 63 | 64 | 65 | # Use a persistent storage volume for sqlite database files and local Active Storage files. 66 | # Recommended to change this to a mounted volume path that is backed up off server. 67 | volumes: 68 | - "easy_wg_mikrotik_storage:/rails/storage" 69 | 70 | 71 | # Bridge fingerprinted assets, like JS and CSS, between versions to avoid 72 | # hitting 404 on in-flight requests. Combines all files from new and old 73 | # version inside the asset_path. 74 | asset_path: /rails/public/assets 75 | 76 | # Configure the image builder. 77 | builder: 78 | arch: amd64 79 | 80 | # # Build image via remote server (useful for faster amd64 builds on arm64 computers) 81 | # remote: ssh://docker@docker-builder-server 82 | # 83 | # # Pass arguments and secrets to the Docker build process 84 | # args: 85 | # RUBY_VERSION: 3.4.2 86 | # secrets: 87 | # - GITHUB_TOKEN 88 | # - RAILS_MASTER_KEY 89 | 90 | # Use a different ssh user than root 91 | # ssh: 92 | # user: app 93 | 94 | # Use accessory services (secrets come from .kamal/secrets). 95 | # accessories: 96 | # db: 97 | # image: mysql:8.0 98 | # host: 192.168.0.2 99 | # # Change to 3306 to expose port to the world instead of just local network. 100 | # port: "127.0.0.1:3306:3306" 101 | # env: 102 | # clear: 103 | # MYSQL_ROOT_HOST: '%' 104 | # secret: 105 | # - MYSQL_ROOT_PASSWORD 106 | # files: 107 | # - config/mysql/production.cnf:/etc/mysql/my.cnf 108 | # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql 109 | # directories: 110 | # - data:/var/lib/mysql 111 | # redis: 112 | # image: redis:7.0 113 | # host: 192.168.0.2 114 | # port: 6379 115 | # directories: 116 | # - data:/data 117 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_for(:title) || t('app.name') %> 5 | 6 | 7 | 8 | <%= csrf_meta_tags %> 9 | <%= csp_meta_tag %> 10 | 11 | <%= yield :head %> 12 | 13 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> 14 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> 15 | 16 | 17 | 18 | 19 | 20 | <%# Bootstrap Icons %> 21 | 22 | 23 | <%# Includes all stylesheet files in app/assets/stylesheets %> 24 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> 25 | <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> 26 | 27 | 28 | 29 |
30 |
31 |
32 | 43 | 44 |
45 | 46 |
47 | 52 | 60 |
61 | 62 | <% if session[:mikrotik_host].present? %> 63 | 69 | <%= link_to logout_path, 70 | class: "wg-btn-danger text-sm px-4 py-2", 71 | data: { turbo_method: :delete, confirm: t('nav.logout') + "?" } do %> 72 | 73 | 74 | <%= t('nav.logout') %> 75 | 76 | <% end %> 77 | <% end %> 78 |
79 |
80 |
81 |
82 | 83 |
84 | <%= yield %> 85 |
86 | <%= render "shared/flash" %> 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/views/authentication/login.html.erb: -------------------------------------------------------------------------------- 1 | 113 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The page you were looking for doesn’t exist (404 Not found) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /config/locales/zh.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | app: 3 | name: "WireGuard MikroTik 管理器" 4 | description: "通过 MikroTik RouterOS 进行 VPN 客户端管理" 5 | 6 | nav: 7 | dashboard: "仪表板" 8 | login: "登录" 9 | logout: "登出" 10 | new_client: "新客户端" 11 | clients: "客户端列表" 12 | language: "语言" 13 | 14 | dashboard: 15 | title: "WireGuard 仪表板" 16 | subtitle: "通过 MikroTik RouterOS 进行 VPN 客户端管理" 17 | new_client_card: 18 | title: "创建新客户端" 19 | description: "创建 WireGuard VPN 客户端并获取二维码" 20 | action: "开始" 21 | badge: "新建" 22 | clients_card: 23 | title: "客户端列表" 24 | description: "查看和管理现有客户端" 25 | action: "管理" 26 | badge: "列表" 27 | connection: 28 | title: "连接状态" 29 | status: "已连接" 30 | host: "主机" 31 | user: "用户" 32 | port: "端口" 33 | guide: 34 | title: "快速入门指南" 35 | step1: 36 | title: "创建客户端" 37 | description: "点击创建新客户端来创建 WireGuard 配置" 38 | step2: 39 | title: "扫描二维码" 40 | description: "使用 WireGuard 应用扫描生成的二维码或下载文件" 41 | step3: 42 | title: "管理客户端" 43 | description: "从客户端列表查看和管理现有客户端" 44 | 45 | login: 46 | title: "MikroTik 登录" 47 | subtitle: "连接到 WireGuard 管理系统" 48 | host_label: "MikroTik 主机" 49 | host_placeholder: "192.168.1.1 或 example.com" 50 | port_label: "端口" 51 | port_placeholder: "8728" 52 | username_label: "用户名" 53 | username_placeholder: "admin" 54 | password_label: "密码" 55 | login_button: "登录" 56 | connecting: "连接中..." 57 | ssl_label: "使用 SSL" 58 | remember_label: "记住登录信息" 59 | api_info: "MikroTik RouterOS API (默认端口: 8728)" 60 | 61 | new_client: 62 | title: "创建新客户端" 63 | subtitle: "配置 WireGuard VPN 客户端并生成二维码" 64 | steps: 65 | interface: "接口设置" 66 | config: "详细配置" 67 | qr: "生成QR" 68 | interface_section: 69 | title: "选择 WireGuard 接口" 70 | label: "选择要使用的接口" 71 | info_title: "接口信息" 72 | public_key: "公钥" 73 | listen_port: "监听端口" 74 | name_section: 75 | title: "设置客户端名称" 76 | label: "输入用于标识此客户端的名称(可选)" 77 | placeholder: "例如:RubyOn-iPhone(如果为空则从分配的 IP 自动生成)" 78 | note: "💡 如果为空则从分配的 IP 自动生成" 79 | config_section: 80 | title: "网络配置" 81 | endpoint_label: "服务器端点" 82 | endpoint_placeholder: "your-server.com:51820" 83 | endpoint_note: "🌐 公网 IP 或域名:端口" 84 | allowed_ips_label: "允许的 IP 范围" 85 | allowed_ips_placeholder: "0.0.0.0/0 或 192.168.1.0/24" 86 | allowed_ips_note: "🔒 客户端可访问的网络" 87 | subnet_label: "客户端 IP 范围" 88 | subnet_placeholder: "10.1.1.0" 89 | subnet_note: "🏷️ 分配给客户端的 IP 范围" 90 | keepalive_label: "Keep Alive(秒)" 91 | keepalive_note: "⏰ 在 NAT 环境中维持连接的间隔" 92 | dns_label: "DNS 服务器" 93 | dns_placeholder: "1.1.1.1, 8.8.8.8" 94 | dns_note: "🌐 要使用的 DNS 服务器(可选,用逗号分隔)" 95 | create_button: "🚀 创建客户端" 96 | cancel_button: "取消" 97 | notice: 98 | title: "重要信息" 99 | mobile: 100 | title: "📱 移动设置" 101 | description: "使用 WireGuard 应用扫描生成的二维码。二维码只显示一次!" 102 | pc: 103 | title: "💻 PC 设置" 104 | description: "下载配置文件并导入到 WireGuard 客户端。" 105 | register: 106 | title: "⚡ 即时注册" 107 | description: "创建客户端后将自动在 MikroTik 路由器上注册对等端。" 108 | manage: 109 | title: "🔧 管理功能" 110 | description: "创建后可以随时在客户端列表中管理和删除。" 111 | 112 | client_result: 113 | title: "客户端创建完成!" 114 | subtitle: "新的 WireGuard 客户端已成功创建" 115 | client_info: "客户端信息" 116 | client_name: "客户端名称" 117 | assigned_ip: "分配 IP" 118 | interface: "接口" 119 | server: "服务器" 120 | created_time: "创建时间" 121 | config_file: "配置文件" 122 | qr_code: "移动二维码" 123 | download_section: "文件下载" 124 | download_config: "下载配置文件" 125 | download_qr: "保存二维码图像" 126 | next_steps: 127 | title: "设置完成 - 下一步" 128 | mobile: 129 | title: "📱 移动设置" 130 | description: "安装 WireGuard 应用并扫描上面的二维码。" 131 | pc: 132 | title: "💻 PC 设置" 133 | description: "安装 WireGuard 客户端并导入配置文件。" 134 | test: 135 | title: "🔗 连接测试" 136 | description: "激活连接并验证 VPN 正常工作。" 137 | manage: 138 | title: "🔧 管理" 139 | description: "如遇到问题,请在客户端列表中管理。" 140 | actions: 141 | create_another: "创建另一个客户端" 142 | view_clients: "查看客户端列表" 143 | back_dashboard: "返回仪表板" 144 | 145 | clients: 146 | title: "客户端列表" 147 | subtitle: "管理您注册的 WireGuard 客户端" 148 | new_client: "创建新客户端" 149 | registered_clients: "已注册客户端" 150 | filter: "过滤器:" 151 | all_interfaces: "所有接口" 152 | reset: "重置" 153 | no_clients: 154 | title: "没有客户端" 155 | description: "创建您的第一个 WireGuard 客户端开始使用。" 156 | action: "创建第一个客户端" 157 | status: 158 | active: "活跃" 159 | waiting: "等待" 160 | fields: 161 | ip_address: "IP 地址" 162 | public_key: "公钥" 163 | keep_alive: "Keep Alive" 164 | last_activity: "最后活动" 165 | never_connected: "从未连接" 166 | not_set: "未设置" 167 | seconds: "秒" 168 | delete_client: "删除客户端" 169 | delete_confirm: "您确定要删除 \"%{name}\" 客户端吗?\n\n此操作无法撤销。" 170 | stats: 171 | total: "总共 %{count} 个客户端" 172 | description: "管理活跃客户端" 173 | refresh: "刷新" 174 | 175 | flash: 176 | login_success: "成功登录到 MikroTik。" 177 | login_failed: "登录失败。请检查您的凭据。" 178 | connection_failed: "连接失败: %{error}" 179 | logout_success: "成功登出。" 180 | login_required: "请先登录到 MikroTik。" 181 | language_changed: "语言更改成功。" 182 | required_fields: "请填写所有字段。" 183 | mikrotik_connection_failed: "连接 MikroTik 失败。" 184 | client_created: "客户端创建成功!" 185 | client_deleted: "客户端删除成功。" 186 | client_delete_failed: "删除客户端失败: %{error}" 187 | no_wireguard_interfaces: "未找到 WireGuard 接口。请先在 MikroTik 中创建 WireGuard 接口。" 188 | wireguard_interface_error: "检索 WireGuard 接口时发生错误。" 189 | interface_required: "请选择 WireGuard 接口。" 190 | config_fields_required: "请填写所有配置字段。" 191 | server_public_key_error: "无法检索服务器公钥。请检查所选的 WireGuard 接口。" 192 | peers_list_error: "无法检索对等端列表。" 193 | peer_registration_failed: "对等端注册失败: %{error}" 194 | client_creation_error: "创建客户端时发生错误: %{error}" 195 | 196 | languages: 197 | ko: "한국어" 198 | en: "English" 199 | zh: "中文" 200 | ja: "日本語" -------------------------------------------------------------------------------- /app/views/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |

<%= t('dashboard.title') %>

4 |

<%= t('dashboard.subtitle') %>

5 |
6 | 7 | 8 |
9 | 10 |
11 | <%= link_to new_client_path, class: "wg-dashboard-card-primary p-8" do %> 12 |
13 |
14 | 15 |
16 |
17 |
<%= t('dashboard.new_client_card.badge') %>
18 |
19 |
20 |

<%= t('dashboard.new_client_card.title') %>

21 |

<%= t('dashboard.new_client_card.description') %>

22 |
23 | <%= t('dashboard.new_client_card.action') %> 24 | 25 |
26 | <% end %> 27 |
28 | 29 | 30 |
31 | <%= link_to clients_path, class: "wg-dashboard-card-secondary p-8" do %> 32 |
33 |
34 | 35 |
36 |
37 |
<%= t('dashboard.clients_card.badge') %>
38 |
39 |
40 |

<%= t('dashboard.clients_card.title') %>

41 |

<%= t('dashboard.clients_card.description') %>

42 |
43 | <%= t('dashboard.clients_card.action') %> 44 | 45 |
46 | <% end %> 47 |
48 |
49 | 50 | 51 |
52 |
53 |

54 |
55 | 56 |
57 | <%= t('dashboard.connection.title') %> 58 |

59 |
60 |
61 | <%= t('dashboard.connection.status') %> 62 |
63 |
64 | 65 |
66 |
67 |
68 | 69 | <%= t('dashboard.connection.host') %> 70 |
71 |
<%= @mikrotik_host %>
72 |
73 | 74 |
75 |
76 | 77 | <%= t('dashboard.connection.user') %> 78 |
79 |
<%= @mikrotik_user %>
80 |
81 | 82 |
83 |
84 | 85 | <%= t('dashboard.connection.port') %> 86 |
87 |
<%= session[:mikrotik_port] %>
88 |
89 |
90 |
91 | 92 | 93 |
94 |
95 |
96 | 97 |
98 |

<%= t('dashboard.guide.title') %>

99 |
100 | 101 |
102 |
103 |
1
104 |

<%= t('dashboard.guide.step1.title') %>

105 |

<%= t('dashboard.guide.step1.description') %>

106 |
107 | 108 |
109 |
2
110 |

<%= t('dashboard.guide.step2.title') %>

111 |

<%= t('dashboard.guide.step2.description') %>

112 |
113 | 114 |
115 |
3
116 |

<%= t('dashboard.guide.step3.title') %>

117 |

<%= t('dashboard.guide.step3.description') %>

118 |
119 |
120 |
121 | -------------------------------------------------------------------------------- /db/queue_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_queue_blocked_executions", force: :cascade do |t| 3 | t.bigint "job_id", null: false 4 | t.string "queue_name", null: false 5 | t.integer "priority", default: 0, null: false 6 | t.string "concurrency_key", null: false 7 | t.datetime "expires_at", null: false 8 | t.datetime "created_at", null: false 9 | t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" 10 | t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" 11 | t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true 12 | end 13 | 14 | create_table "solid_queue_claimed_executions", force: :cascade do |t| 15 | t.bigint "job_id", null: false 16 | t.bigint "process_id" 17 | t.datetime "created_at", null: false 18 | t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true 19 | t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" 20 | end 21 | 22 | create_table "solid_queue_failed_executions", force: :cascade do |t| 23 | t.bigint "job_id", null: false 24 | t.text "error" 25 | t.datetime "created_at", null: false 26 | t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true 27 | end 28 | 29 | create_table "solid_queue_jobs", force: :cascade do |t| 30 | t.string "queue_name", null: false 31 | t.string "class_name", null: false 32 | t.text "arguments" 33 | t.integer "priority", default: 0, null: false 34 | t.string "active_job_id" 35 | t.datetime "scheduled_at" 36 | t.datetime "finished_at" 37 | t.string "concurrency_key" 38 | t.datetime "created_at", null: false 39 | t.datetime "updated_at", null: false 40 | t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" 41 | t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" 42 | t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" 43 | t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" 44 | t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" 45 | end 46 | 47 | create_table "solid_queue_pauses", force: :cascade do |t| 48 | t.string "queue_name", null: false 49 | t.datetime "created_at", null: false 50 | t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true 51 | end 52 | 53 | create_table "solid_queue_processes", force: :cascade do |t| 54 | t.string "kind", null: false 55 | t.datetime "last_heartbeat_at", null: false 56 | t.bigint "supervisor_id" 57 | t.integer "pid", null: false 58 | t.string "hostname" 59 | t.text "metadata" 60 | t.datetime "created_at", null: false 61 | t.string "name", null: false 62 | t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" 63 | t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true 64 | t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" 65 | end 66 | 67 | create_table "solid_queue_ready_executions", force: :cascade do |t| 68 | t.bigint "job_id", null: false 69 | t.string "queue_name", null: false 70 | t.integer "priority", default: 0, null: false 71 | t.datetime "created_at", null: false 72 | t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true 73 | t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" 74 | t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" 75 | end 76 | 77 | create_table "solid_queue_recurring_executions", force: :cascade do |t| 78 | t.bigint "job_id", null: false 79 | t.string "task_key", null: false 80 | t.datetime "run_at", null: false 81 | t.datetime "created_at", null: false 82 | t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true 83 | t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true 84 | end 85 | 86 | create_table "solid_queue_recurring_tasks", force: :cascade do |t| 87 | t.string "key", null: false 88 | t.string "schedule", null: false 89 | t.string "command", limit: 2048 90 | t.string "class_name" 91 | t.text "arguments" 92 | t.string "queue_name" 93 | t.integer "priority", default: 0 94 | t.boolean "static", default: true, null: false 95 | t.text "description" 96 | t.datetime "created_at", null: false 97 | t.datetime "updated_at", null: false 98 | t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true 99 | t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" 100 | end 101 | 102 | create_table "solid_queue_scheduled_executions", force: :cascade do |t| 103 | t.bigint "job_id", null: false 104 | t.string "queue_name", null: false 105 | t.integer "priority", default: 0, null: false 106 | t.datetime "scheduled_at", null: false 107 | t.datetime "created_at", null: false 108 | t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true 109 | t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" 110 | end 111 | 112 | create_table "solid_queue_semaphores", force: :cascade do |t| 113 | t.string "key", null: false 114 | t.integer "value", default: 1, null: false 115 | t.datetime "expires_at", null: false 116 | t.datetime "created_at", null: false 117 | t.datetime "updated_at", null: false 118 | t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" 119 | t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" 120 | t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true 121 | end 122 | 123 | add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 124 | add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 125 | add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 126 | add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 127 | add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 128 | add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 129 | end 130 | -------------------------------------------------------------------------------- /config/locales/ko.yml: -------------------------------------------------------------------------------- 1 | ko: 2 | app: 3 | name: "WireGuard MikroTik 관리" 4 | description: "MikroTik RouterOS를 통한 VPN 클라이언트 관리" 5 | 6 | nav: 7 | dashboard: "대시보드" 8 | login: "로그인" 9 | logout: "로그아웃" 10 | new_client: "새 클라이언트" 11 | clients: "클라이언트 목록" 12 | language: "언어" 13 | 14 | dashboard: 15 | title: "WireGuard 대시보드" 16 | subtitle: "MikroTik RouterOS를 통한 VPN 클라이언트 관리" 17 | new_client_card: 18 | title: "새 클라이언트 생성" 19 | description: "WireGuard VPN 클라이언트를 생성하고 QR 코드를 받으세요" 20 | action: "시작하기" 21 | badge: "신규" 22 | clients_card: 23 | title: "클라이언트 목록" 24 | description: "기존 클라이언트를 조회하고 관리하세요" 25 | action: "관리하기" 26 | badge: "목록" 27 | connection: 28 | title: "연결 상태" 29 | status: "연결됨" 30 | host: "호스트" 31 | user: "사용자" 32 | port: "포트" 33 | guide: 34 | title: "빠른 시작 가이드" 35 | step1: 36 | title: "클라이언트 생성" 37 | description: "새 클라이언트 생성을 클릭하여 WireGuard 설정을 만드세요" 38 | step2: 39 | title: "QR 코드 스캔" 40 | description: "생성된 QR 코드를 WireGuard 앱으로 스캔하거나 파일을 다운로드하세요" 41 | step3: 42 | title: "클라이언트 관리" 43 | description: "클라이언트 목록에서 기존 클라이언트를 조회하고 관리하세요" 44 | 45 | login: 46 | title: "MikroTik 로그인" 47 | subtitle: "WireGuard 관리 시스템에 접속하세요" 48 | host_label: "MikroTik 호스트" 49 | host_placeholder: "192.168.1.1 또는 example.com" 50 | port_label: "포트" 51 | port_placeholder: "8728" 52 | username_label: "사용자명" 53 | username_placeholder: "admin" 54 | password_label: "비밀번호" 55 | login_button: "로그인" 56 | connecting: "연결 중..." 57 | ssl_label: "SSL 사용" 58 | remember_label: "로그인 정보 저장하기" 59 | api_info: "MikroTik RouterOS API (기본 포트: 8728)" 60 | 61 | new_client: 62 | title: "새 클라이언트 생성" 63 | subtitle: "WireGuard VPN 클라이언트를 설정하고 QR 코드를 생성하세요" 64 | steps: 65 | interface: "인터페이스" 66 | config: "설정 구성" 67 | qr: "QR 생성" 68 | interface_section: 69 | title: "WireGuard 인터페이스 선택" 70 | label: "사용할 인터페이스를 선택하세요" 71 | info_title: "인터페이스 정보" 72 | public_key: "공개키" 73 | listen_port: "Listen Port" 74 | name_section: 75 | title: "클라이언트 이름 설정" 76 | label: "클라이언트를 식별할 이름을 입력하세요 (선택사항)" 77 | placeholder: "예: RubyOn-iPhone (비워두면 할당받은 IP로 자동 생성)" 78 | note: "💡 비워두면 할당받은 아이피로 자동 생성됩니다" 79 | config_section: 80 | title: "네트워크 설정 구성" 81 | endpoint_label: "서버 엔드포인트" 82 | endpoint_placeholder: "your-server.com:51820" 83 | endpoint_note: "🌐 공인 IP 또는 도메인:포트" 84 | allowed_ips_label: "허용된 IP 대역" 85 | allowed_ips_placeholder: "0.0.0.0/0 또는 192.168.1.0/24" 86 | allowed_ips_note: "🔒 클라이언트가 접근할 수 있는 네트워크" 87 | subnet_label: "클라이언트 IP 대역" 88 | subnet_placeholder: "10.1.1.0" 89 | subnet_note: "🏷️ 클라이언트에게 할당할 IP 범위" 90 | keepalive_label: "Keep Alive (초)" 91 | keepalive_note: "⏰ NAT 환경에서 연결 유지를 위한 주기" 92 | dns_label: "DNS 서버" 93 | dns_placeholder: "1.1.1.1, 8.8.8.8" 94 | dns_note: "🌐 사용할 DNS 서버 (선택사항, 쉼표로 구분)" 95 | create_button: "🚀 클라이언트 생성하기" 96 | cancel_button: "취소" 97 | notice: 98 | title: "중요한 안내사항" 99 | mobile: 100 | title: "📱 모바일 설정" 101 | description: "생성된 QR 코드를 WireGuard 앱으로 스캔하세요. 코드는 한 번만 표시됩니다!" 102 | pc: 103 | title: "💻 PC 설정" 104 | description: "설정 파일을 다운로드하여 WireGuard 클라이언트에서 import하세요." 105 | register: 106 | title: "⚡ 즉시 등록" 107 | description: "클라이언트 생성과 동시에 MikroTik 라우터에 피어가 등록됩니다." 108 | manage: 109 | title: "🔧 관리 기능" 110 | description: "생성 후 클라이언트 목록에서 언제든 관리하고 삭제할 수 있습니다." 111 | 112 | client_result: 113 | title: "클라이언트 생성 완료!" 114 | subtitle: "새 WireGuard 클라이언트가 성공적으로 생성되었습니다" 115 | client_info: "클라이언트 정보" 116 | client_name: "클라이언트명" 117 | assigned_ip: "할당 IP" 118 | interface: "인터페이스" 119 | server: "서버" 120 | created_time: "생성시간" 121 | config_file: "설정 파일" 122 | qr_code: "모바일 QR 코드" 123 | download_section: "파일 다운로드" 124 | download_config: "설정 파일 다운로드" 125 | download_qr: "QR 코드 이미지 저장" 126 | next_steps: 127 | title: "설정 완료 - 다음 단계" 128 | mobile: 129 | title: "📱 모바일 설정" 130 | description: "WireGuard 앱을 설치하고 위의 QR 코드를 스캔하세요." 131 | pc: 132 | title: "💻 PC 설정" 133 | description: "WireGuard 클라이언트를 설치하고 설정 파일을 import하세요." 134 | test: 135 | title: "🔗 연결 테스트" 136 | description: "연결을 활성화하고 VPN이 정상 작동하는지 확인하세요." 137 | manage: 138 | title: "🔧 관리" 139 | description: "문제가 있으면 클라이언트 목록에서 관리하세요." 140 | actions: 141 | create_another: "다른 클라이언트 생성" 142 | view_clients: "클라이언트 목록 보기" 143 | back_dashboard: "대시보드로 돌아가기" 144 | 145 | clients: 146 | title: "클라이언트 목록" 147 | subtitle: "등록된 WireGuard 클라이언트들을 관리하세요" 148 | new_client: "새 클라이언트 생성" 149 | registered_clients: "등록된 클라이언트" 150 | filter: "필터:" 151 | all_interfaces: "모든 인터페이스" 152 | reset: "초기화" 153 | no_clients: 154 | title: "클라이언트가 없습니다" 155 | description: "첫 번째 WireGuard 클라이언트를 생성해서 시작해보세요." 156 | action: "첫 클라이언트 생성하기" 157 | status: 158 | active: "활성" 159 | waiting: "대기" 160 | fields: 161 | ip_address: "IP 주소" 162 | public_key: "공개키" 163 | keep_alive: "Keep Alive" 164 | last_activity: "마지막 활동" 165 | never_connected: "연결된 적 없음" 166 | not_set: "설정안함" 167 | seconds: "초" 168 | delete_client: "클라이언트 삭제" 169 | delete_confirm: "정말로 \"%{name}\" 클라이언트를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다." 170 | stats: 171 | total: "총 %{count}개의 클라이언트" 172 | description: "활성 상태인 클라이언트들을 관리하세요" 173 | refresh: "새로고침" 174 | 175 | flash: 176 | login_success: "MikroTik에 성공적으로 로그인했습니다." 177 | login_failed: "로그인에 실패했습니다. 정보를 확인해주세요." 178 | connection_failed: "연결에 실패했습니다: %{error}" 179 | logout_success: "로그아웃되었습니다." 180 | login_required: "먼저 MikroTik에 로그인해주세요." 181 | language_changed: "언어가 변경되었습니다." 182 | required_fields: "모든 필드를 입력해주세요." 183 | mikrotik_connection_failed: "MikroTik 연결에 실패했습니다." 184 | client_created: "새 클라이언트가 성공적으로 생성되었습니다!" 185 | client_deleted: "클라이언트가 성공적으로 삭제되었습니다." 186 | client_delete_failed: "클라이언트 삭제에 실패했습니다: %{error}" 187 | no_wireguard_interfaces: "WireGuard 인터페이스가 없습니다. MikroTik에서 WireGuard 인터페이스를 먼저 생성해주세요." 188 | wireguard_interface_error: "WireGuard 인터페이스 조회 중 오류가 발생했습니다." 189 | interface_required: "WireGuard 인터페이스를 선택해주세요." 190 | config_fields_required: "모든 설정 필드를 입력해주세요." 191 | server_public_key_error: "서버 공개키를 가져올 수 없습니다. 선택한 WireGuard 인터페이스를 확인해주세요." 192 | peers_list_error: "피어 목록을 가져올 수 없습니다." 193 | peer_registration_failed: "피어 등록에 실패했습니다: %{error}" 194 | client_creation_error: "클라이언트 생성 중 오류가 발생했습니다: %{error}" 195 | 196 | languages: 197 | ko: "한국어" 198 | en: "English" 199 | zh: "中文" 200 | ja: "日本語" -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | app: 3 | name: "WireGuard MikroTik マネージャー" 4 | description: "MikroTik RouterOS を通じた VPN クライアント管理" 5 | 6 | nav: 7 | dashboard: "ダッシュボード" 8 | login: "ログイン" 9 | logout: "ログアウト" 10 | new_client: "新しいクライアント" 11 | clients: "クライアント一覧" 12 | language: "言語" 13 | 14 | dashboard: 15 | title: "WireGuard ダッシュボード" 16 | subtitle: "MikroTik RouterOS を通じた VPN クライアント管理" 17 | new_client_card: 18 | title: "新しいクライアントの作成" 19 | description: "WireGuard VPN クライアントを作成し、QR コードを取得" 20 | action: "開始" 21 | badge: "新規" 22 | clients_card: 23 | title: "クライアント一覧" 24 | description: "既存のクライアントを表示・管理" 25 | action: "管理" 26 | badge: "一覧" 27 | connection: 28 | title: "接続状態" 29 | status: "接続済み" 30 | host: "ホスト" 31 | user: "ユーザー" 32 | port: "ポート" 33 | guide: 34 | title: "クイックスタートガイド" 35 | step1: 36 | title: "クライアント作成" 37 | description: "新しいクライアント作成をクリックして WireGuard 設定を作成" 38 | step2: 39 | title: "QRコードスキャン" 40 | description: "生成されたQRコードをWireGuardアプリでスキャンまたはファイルをダウンロード" 41 | step3: 42 | title: "クライアント管理" 43 | description: "クライアント一覧から既存のクライアントを表示・管理" 44 | 45 | login: 46 | title: "MikroTik ログイン" 47 | subtitle: "WireGuard 管理システムに接続" 48 | host_label: "MikroTik ホスト" 49 | host_placeholder: "192.168.1.1 または example.com" 50 | port_label: "ポート" 51 | port_placeholder: "8728" 52 | username_label: "ユーザー名" 53 | username_placeholder: "admin" 54 | password_label: "パスワード" 55 | login_button: "ログイン" 56 | connecting: "接続中..." 57 | ssl_label: "SSL を使用" 58 | remember_label: "ログイン情報を保存" 59 | api_info: "MikroTik RouterOS API (デフォルトポート: 8728)" 60 | 61 | new_client: 62 | title: "新しいクライアントの作成" 63 | subtitle: "WireGuard VPN クライアントを設定し、QRコードを生成" 64 | steps: 65 | interface: "接続設定" 66 | config: "詳細設定" 67 | qr: "QR生成" 68 | interface_section: 69 | title: "WireGuard インターフェースを選択" 70 | label: "使用するインターフェースを選択してください" 71 | info_title: "インターフェース情報" 72 | public_key: "公開鍵" 73 | listen_port: "リスンポート" 74 | name_section: 75 | title: "クライアント名を設定" 76 | label: "このクライアントを識別する名前を入力(オプション)" 77 | placeholder: "例:RubyOn-iPhone(空の場合は割り当てられたIPから自動生成)" 78 | note: "💡 空の場合は割り当てられたIPから自動生成されます" 79 | config_section: 80 | title: "ネットワーク設定" 81 | endpoint_label: "サーバーエンドポイント" 82 | endpoint_placeholder: "your-server.com:51820" 83 | endpoint_note: "🌐 パブリックIPまたはドメイン:ポート" 84 | allowed_ips_label: "許可されたIP範囲" 85 | allowed_ips_placeholder: "0.0.0.0/0 または 192.168.1.0/24" 86 | allowed_ips_note: "🔒 クライアントがアクセスできるネットワーク" 87 | subnet_label: "クライアントIP範囲" 88 | subnet_placeholder: "10.1.1.0" 89 | subnet_note: "🏷️ クライアントに割り当てるIP範囲" 90 | keepalive_label: "Keep Alive(秒)" 91 | keepalive_note: "⏰ NAT環境での接続維持の間隔" 92 | dns_label: "DNSサーバー" 93 | dns_placeholder: "1.1.1.1, 8.8.8.8" 94 | dns_note: "🌐 使用するDNSサーバー(任意、カンマ区切り)" 95 | create_button: "🚀 クライアント作成" 96 | cancel_button: "キャンセル" 97 | notice: 98 | title: "重要な情報" 99 | mobile: 100 | title: "📱 モバイル設定" 101 | description: "生成されたQRコードをWireGuardアプリでスキャンしてください。コードは一度だけ表示されます!" 102 | pc: 103 | title: "💻 PC設定" 104 | description: "設定ファイルをダウンロードし、WireGuardクライアントでインポートしてください。" 105 | register: 106 | title: "⚡ 即座登録" 107 | description: "クライアント作成と同時にMikroTikルーターにピアが登録されます。" 108 | manage: 109 | title: "🔧 管理機能" 110 | description: "作成後はクライアント一覧からいつでも管理や削除が可能です。" 111 | 112 | client_result: 113 | title: "クライアント作成完了!" 114 | subtitle: "新しいWireGuardクライアントが正常に作成されました" 115 | client_info: "クライアント情報" 116 | client_name: "クライアント名" 117 | assigned_ip: "割り当てIP" 118 | interface: "インターフェース" 119 | server: "サーバー" 120 | created_time: "作成時間" 121 | config_file: "設定ファイル" 122 | qr_code: "モバイルQRコード" 123 | download_section: "ファイルダウンロード" 124 | download_config: "設定ファイルをダウンロード" 125 | download_qr: "QRコード画像を保存" 126 | next_steps: 127 | title: "設定完了 - 次のステップ" 128 | mobile: 129 | title: "📱 モバイル設定" 130 | description: "WireGuardアプリをインストールし、上記のQRコードをスキャンしてください。" 131 | pc: 132 | title: "💻 PC設定" 133 | description: "WireGuardクライアントをインストールし、設定ファイルをインポートしてください。" 134 | test: 135 | title: "🔗 接続テスト" 136 | description: "接続をアクティベートし、VPNが正常に動作することを確認してください。" 137 | manage: 138 | title: "🔧 管理" 139 | description: "問題があればクライアント一覧から管理してください。" 140 | actions: 141 | create_another: "別のクライアントを作成" 142 | view_clients: "クライアント一覧を表示" 143 | back_dashboard: "ダッシュボードに戻る" 144 | 145 | clients: 146 | title: "クライアント一覧" 147 | subtitle: "登録されたWireGuardクライアントを管理" 148 | new_client: "新しいクライアントを作成" 149 | registered_clients: "登録済みクライアント" 150 | filter: "フィルター:" 151 | all_interfaces: "すべてのインターフェース" 152 | reset: "リセット" 153 | no_clients: 154 | title: "クライアントがありません" 155 | description: "最初のWireGuardクライアントを作成して始めましょう。" 156 | action: "最初のクライアントを作成" 157 | status: 158 | active: "アクティブ" 159 | waiting: "待機中" 160 | fields: 161 | ip_address: "IPアドレス" 162 | public_key: "公開鍵" 163 | keep_alive: "Keep Alive" 164 | last_activity: "最終アクティビティ" 165 | never_connected: "接続したことがありません" 166 | not_set: "設定されていません" 167 | seconds: "秒" 168 | delete_client: "クライアントを削除" 169 | delete_confirm: "\"%{name}\"クライアントを削除してもよろしいですか?\n\nこの操作は元に戻せません。" 170 | stats: 171 | total: "合計%{count}個のクライアント" 172 | description: "アクティブなクライアントを管理" 173 | refresh: "更新" 174 | 175 | flash: 176 | login_success: "MikroTik への ログインに成功しました。" 177 | login_failed: "ログインに失敗しました。認証情報を確認してください。" 178 | connection_failed: "接続に失敗しました: %{error}" 179 | logout_success: "ログアウトしました。" 180 | login_required: "まず MikroTik にログインしてください。" 181 | language_changed: "言語が正常に変更されました。" 182 | required_fields: "すべてのフィールドを入力してください。" 183 | mikrotik_connection_failed: "MikroTikへの接続に失敗しました。" 184 | client_created: "クライアントが正常に作成されました!" 185 | client_deleted: "クライアントが正常に削除されました。" 186 | client_delete_failed: "クライアントの削除に失敗しました: %{error}" 187 | no_wireguard_interfaces: "WireGuard インターフェースが見つかりません。まず MikroTik で WireGuard インターフェースを作成してください。" 188 | wireguard_interface_error: "WireGuard インターフェースの取得中にエラーが発生しました。" 189 | interface_required: "WireGuard インターフェースを選択してください。" 190 | config_fields_required: "すべての設定フィールドを入力してください。" 191 | server_public_key_error: "サーバー公開鍵を取得できません。選択した WireGuard インターフェースを確認してください。" 192 | peers_list_error: "ピア一覧を取得できません。" 193 | peer_registration_failed: "ピア登録に失敗しました: %{error}" 194 | client_creation_error: "クライアント作成中にエラーが発生しました: %{error}" 195 | 196 | languages: 197 | ko: "한국어" 198 | en: "English" 199 | zh: "中文" 200 | ja: "日本語" -------------------------------------------------------------------------------- /public/400.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The server cannot process the request due to a client error (400 Bad Request) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/406-unsupported-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Your browser is not supported (406 Not Acceptable) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

Your browser is not supported.
Please upgrade your browser to continue.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /.rubocop_naming.yml: -------------------------------------------------------------------------------- 1 | # Department 'Naming' (18): 수정 2 | Naming/AccessorMethodName: 3 | Description: Check the naming of accessor methods for get_/set_. 4 | StyleGuide: "#accessor_mutator_method_names" 5 | Enabled: true 6 | VersionAdded: '0.50' 7 | 8 | Naming/AsciiIdentifiers: 9 | Description: Use only ascii symbols in identifiers and constants. 10 | StyleGuide: "#english-identifiers" 11 | Enabled: true 12 | VersionAdded: '0.50' 13 | VersionChanged: '0.87' 14 | AsciiConstants: true 15 | 16 | # Supports --autocorrect 17 | Naming/BinaryOperatorParameterName: 18 | Description: When defining binary operators, name the argument other. 19 | StyleGuide: "#other-arg" 20 | Enabled: true 21 | VersionAdded: '0.50' 22 | VersionChanged: '1.2' 23 | 24 | # Supports --autocorrect 25 | Naming/BlockForwarding: 26 | Description: Use anonymous block forwarding. 27 | StyleGuide: "#block-forwarding" 28 | Enabled: true 29 | VersionAdded: '1.24' 30 | EnforcedStyle: anonymous 31 | SupportedStyles: 32 | - anonymous 33 | - explicit 34 | BlockForwardingName: block 35 | 36 | Naming/BlockParameterName: 37 | Description: Checks for block parameter names that contain capital letters, end in 38 | numbers, or do not meet a minimal length. 39 | Enabled: true 40 | VersionAdded: '0.53' 41 | VersionChanged: '0.77' 42 | MinNameLength: 1 43 | AllowNamesEndingInNumbers: true 44 | AllowedNames: [] 45 | ForbiddenNames: [] 46 | 47 | Naming/ClassAndModuleCamelCase: 48 | Description: Use CamelCase for classes and modules. 49 | StyleGuide: "#camelcase-classes" 50 | Enabled: true 51 | VersionAdded: '0.50' 52 | VersionChanged: '0.85' 53 | AllowedNames: 54 | - module_parent 55 | 56 | Naming/ConstantName: 57 | Description: Constants should use SCREAMING_SNAKE_CASE. 58 | StyleGuide: "#screaming-snake-case" 59 | Enabled: true 60 | VersionAdded: '0.50' 61 | 62 | Naming/FileName: 63 | Description: Use snake_case for source file names. 64 | StyleGuide: "#snake-case-files" 65 | Enabled: true 66 | VersionAdded: '0.50' 67 | VersionChanged: '1.23' 68 | Exclude: 69 | - "/Users/rubyon/Desktop/liaf-rails/Rakefile.rb" 70 | ExpectMatchingDefinition: false 71 | CheckDefinitionPathHierarchy: true 72 | CheckDefinitionPathHierarchyRoots: 73 | - lib 74 | - spec 75 | - test 76 | - src 77 | Regex: 78 | IgnoreExecutableScripts: true 79 | AllowedAcronyms: 80 | - CLI 81 | - DSL 82 | - ACL 83 | - API 84 | - ASCII 85 | - CPU 86 | - CSS 87 | - DNS 88 | - EOF 89 | - GUID 90 | - HTML 91 | - HTTP 92 | - HTTPS 93 | - ID 94 | - IP 95 | - JSON 96 | - LHS 97 | - QPS 98 | - RAM 99 | - RHS 100 | - RPC 101 | - SLA 102 | - SMTP 103 | - SQL 104 | - SSH 105 | - TCP 106 | - TLS 107 | - TTL 108 | - UDP 109 | - UI 110 | - UID 111 | - UUID 112 | - URI 113 | - URL 114 | - UTF8 115 | - VM 116 | - XML 117 | - XMPP 118 | - XSRF 119 | - XSS 120 | 121 | # Supports --autocorrect 122 | Naming/HeredocDelimiterCase: 123 | Description: Use configured case for heredoc delimiters. 124 | StyleGuide: "#heredoc-delimiters" 125 | Enabled: true 126 | VersionAdded: '0.50' 127 | VersionChanged: '1.2' 128 | EnforcedStyle: uppercase 129 | SupportedStyles: 130 | - lowercase 131 | - uppercase 132 | 133 | Naming/HeredocDelimiterNaming: 134 | Description: Use descriptive heredoc delimiters. 135 | StyleGuide: "#heredoc-delimiters" 136 | Enabled: true 137 | VersionAdded: '0.50' 138 | ForbiddenDelimiters: 139 | - !ruby/regexp /(^|\s)(EO[A-Z]{1}|END)(\s|$)/i 140 | 141 | # Supports --autocorrect 142 | Naming/InclusiveLanguage: 143 | Description: Recommend the use of inclusive language instead of problematic terms. 144 | Enabled: false 145 | VersionAdded: '1.18' 146 | VersionChanged: '1.49' 147 | CheckIdentifiers: true 148 | CheckConstants: true 149 | CheckVariables: true 150 | CheckStrings: false 151 | CheckSymbols: true 152 | CheckComments: true 153 | CheckFilepaths: true 154 | FlaggedTerms: 155 | whitelist: 156 | Regex: !ruby/regexp /white[-_\s]?list/ 157 | Suggestions: 158 | - allowlist 159 | - permit 160 | blacklist: 161 | Regex: !ruby/regexp /black[-_\s]?list/ 162 | Suggestions: 163 | - denylist 164 | - block 165 | slave: 166 | WholeWord: true 167 | Suggestions: 168 | - replica 169 | - secondary 170 | - follower 171 | 172 | # Supports --autocorrect 173 | Naming/MemoizedInstanceVariableName: 174 | Description: Memoized method name should match memo instance variable name. 175 | Enabled: true 176 | VersionAdded: '0.53' 177 | VersionChanged: '1.2' 178 | EnforcedStyleForLeadingUnderscores: disallowed 179 | SupportedStylesForLeadingUnderscores: 180 | - disallowed 181 | - required 182 | - optional 183 | Safe: false 184 | 185 | Naming/MethodName: 186 | Description: Use the configured style when naming methods. 187 | StyleGuide: "#snake-case-symbols-methods-vars" 188 | Enabled: true 189 | VersionAdded: '0.50' 190 | EnforcedStyle: snake_case 191 | SupportedStyles: 192 | - snake_case 193 | - camelCase 194 | AllowedPatterns: [] 195 | 196 | Naming/MethodParameterName: 197 | Description: Checks for method parameter names that contain capital letters, end in 198 | numbers, or do not meet a minimal length. 199 | Enabled: true 200 | VersionAdded: '0.53' 201 | VersionChanged: '0.77' 202 | MinNameLength: 3 203 | AllowNamesEndingInNumbers: true 204 | AllowedNames: 205 | - as 206 | - at 207 | - by 208 | - cc 209 | - db 210 | - id 211 | - if 212 | - in 213 | - io 214 | - ip 215 | - of 216 | - 'on' 217 | - os 218 | - pp 219 | - to 220 | ForbiddenNames: [] 221 | 222 | Naming/PredicatePrefix: 223 | Description: Check the names of predicate methods. 224 | StyleGuide: "#bool-methods-qmark" 225 | Enabled: false 226 | VersionAdded: '0.50' 227 | VersionChanged: '0.77' 228 | NamePrefix: 229 | - is_ 230 | - has_ 231 | - have_ 232 | ForbiddenPrefixes: 233 | - is_ 234 | - has_ 235 | - have_ 236 | AllowedMethods: 237 | - is_a? 238 | MethodDefinitionMacros: 239 | - define_method 240 | - define_singleton_method 241 | Exclude: 242 | - "/Users/rubyon/Desktop/liaf-rails/spec/**/*" 243 | 244 | # Supports --autocorrect 245 | Naming/RescuedExceptionsVariableName: 246 | Description: Use consistent rescued exceptions variables naming. 247 | Enabled: true 248 | VersionAdded: '0.67' 249 | VersionChanged: '0.68' 250 | PreferredName: e 251 | 252 | Naming/VariableName: 253 | Description: Use the configured style when naming variables. 254 | StyleGuide: "#snake-case-symbols-methods-vars" 255 | Enabled: true 256 | VersionAdded: '0.50' 257 | VersionChanged: '1.8' 258 | EnforcedStyle: snake_case 259 | SupportedStyles: 260 | - snake_case 261 | - camelCase 262 | AllowedIdentifiers: [] 263 | AllowedPatterns: [] 264 | 265 | Naming/VariableNumber: 266 | Description: Use the configured style when numbering symbols, methods and variables. 267 | StyleGuide: "#snake-case-symbols-methods-vars-with-numbers" 268 | Enabled: true 269 | VersionAdded: '0.50' 270 | VersionChanged: '1.4' 271 | EnforcedStyle: normalcase 272 | SupportedStyles: 273 | - snake_case 274 | - normalcase 275 | - non_integer 276 | CheckMethodNames: true 277 | CheckSymbols: true 278 | AllowedIdentifiers: 279 | - capture3 280 | - iso8601 281 | - rfc1123_date 282 | - rfc822 283 | - rfc2822 284 | - rfc3339 285 | - x86_64 286 | AllowedPatterns: [] -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | We’re sorry, but something went wrong (500 Internal Server Error) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | app: 3 | name: "WireGuard MikroTik Manager" 4 | description: "VPN Client Management through MikroTik RouterOS" 5 | 6 | nav: 7 | dashboard: "Dashboard" 8 | login: "Login" 9 | logout: "Logout" 10 | new_client: "New Client" 11 | clients: "Client List" 12 | language: "Language" 13 | 14 | dashboard: 15 | title: "WireGuard Dashboard" 16 | subtitle: "VPN Client Management through MikroTik RouterOS" 17 | new_client_card: 18 | title: "Create New Client" 19 | description: "Create a WireGuard VPN client and get QR code" 20 | action: "Get Started" 21 | badge: "NEW" 22 | clients_card: 23 | title: "Client List" 24 | description: "View and manage existing clients" 25 | action: "Manage" 26 | badge: "LIST" 27 | connection: 28 | title: "Connection Status" 29 | status: "Connected" 30 | host: "Host" 31 | user: "User" 32 | port: "Port" 33 | guide: 34 | title: "Quick Start Guide" 35 | step1: 36 | title: "Create Client" 37 | description: "Click on Create New Client to create WireGuard configuration" 38 | step2: 39 | title: "Scan QR Code" 40 | description: "Scan the generated QR code with WireGuard app or download the file" 41 | step3: 42 | title: "Manage Clients" 43 | description: "View and manage existing clients from the client list" 44 | 45 | login: 46 | title: "MikroTik Login" 47 | subtitle: "Connect to WireGuard Management System" 48 | host_label: "MikroTik Host" 49 | host_placeholder: "192.168.1.1 or example.com" 50 | port_label: "Port" 51 | port_placeholder: "8728" 52 | username_label: "Username" 53 | username_placeholder: "admin" 54 | password_label: "Password" 55 | login_button: "Login" 56 | connecting: "Connecting..." 57 | ssl_label: "Use SSL" 58 | remember_label: "Remember login info" 59 | api_info: "MikroTik RouterOS API (Default port: 8728)" 60 | 61 | flash: 62 | login_success: "Successfully logged in to MikroTik." 63 | login_failed: "Login failed. Please check your credentials." 64 | connection_failed: "Connection failed: %{error}" 65 | logout_success: "Logged out successfully." 66 | login_required: "Please log in to MikroTik first." 67 | language_changed: "Language changed successfully." 68 | required_fields: "Please fill in all fields." 69 | mikrotik_connection_failed: "Failed to connect to MikroTik." 70 | client_created: "New client created successfully!" 71 | client_deleted: "Client deleted successfully." 72 | client_delete_failed: "Failed to delete client: %{error}" 73 | no_wireguard_interfaces: "No WireGuard interfaces found. Please create a WireGuard interface in MikroTik first." 74 | wireguard_interface_error: "An error occurred while retrieving WireGuard interfaces." 75 | interface_required: "Please select a WireGuard interface." 76 | config_fields_required: "Please fill in all configuration fields." 77 | server_public_key_error: "Unable to retrieve server public key. Please check the selected WireGuard interface." 78 | peers_list_error: "Unable to retrieve peers list." 79 | peer_registration_failed: "Failed to register peer: %{error}" 80 | client_creation_error: "An error occurred while creating client: %{error}" 81 | 82 | new_client: 83 | title: "Create New Client" 84 | subtitle: "Configure WireGuard VPN client and generate QR code" 85 | steps: 86 | interface: "Interface" 87 | config: "Configuration" 88 | qr: "QR Code" 89 | interface_section: 90 | title: "Select WireGuard Interface" 91 | label: "Choose the interface to use" 92 | info_title: "Interface Information" 93 | public_key: "Public Key" 94 | listen_port: "Listen Port" 95 | name_section: 96 | title: "Set Client Name" 97 | label: "Enter a name to identify this client (optional)" 98 | placeholder: "e.g., RubyOn-iPhone (auto-generated from assigned IP if empty)" 99 | note: "💡 Will be auto-generated from assigned IP if left empty" 100 | config_section: 101 | title: "Network Configuration" 102 | endpoint_label: "Server Endpoint" 103 | endpoint_placeholder: "your-server.com:51820" 104 | endpoint_note: "🌐 Public IP or domain:port" 105 | allowed_ips_label: "Allowed IP Ranges" 106 | allowed_ips_placeholder: "0.0.0.0/0 or 192.168.1.0/24" 107 | allowed_ips_note: "🔒 Networks that client can access" 108 | subnet_label: "Client IP Range" 109 | subnet_placeholder: "10.1.1.0" 110 | subnet_note: "🏷️ IP range to assign to clients" 111 | keepalive_label: "Keep Alive (seconds)" 112 | keepalive_note: "⏰ Interval for maintaining connection in NAT environments" 113 | dns_label: "DNS Servers" 114 | dns_placeholder: "1.1.1.1, 8.8.8.8" 115 | dns_note: "🌐 DNS servers to use (optional, comma-separated)" 116 | create_button: "🚀 Create Client" 117 | cancel_button: "Cancel" 118 | notice: 119 | title: "Important Information" 120 | mobile: 121 | title: "📱 Mobile Setup" 122 | description: "Scan the generated QR code with WireGuard app. Code is shown only once!" 123 | pc: 124 | title: "💻 PC Setup" 125 | description: "Download configuration file and import into WireGuard client." 126 | register: 127 | title: "⚡ Instant Registration" 128 | description: "Peer is automatically registered on MikroTik router upon client creation." 129 | manage: 130 | title: "🔧 Management Features" 131 | description: "You can manage and delete clients anytime from the client list." 132 | 133 | client_result: 134 | title: "Client Creation Complete!" 135 | subtitle: "New WireGuard client has been successfully created" 136 | client_info: "Client Information" 137 | client_name: "Client Name" 138 | assigned_ip: "Assigned IP" 139 | interface: "Interface" 140 | server: "Server" 141 | created_time: "Created Time" 142 | config_file: "Configuration File" 143 | qr_code: "Mobile QR Code" 144 | download_section: "File Downloads" 145 | download_config: "Download Configuration File" 146 | download_qr: "Save QR Code Image" 147 | next_steps: 148 | title: "Setup Complete - Next Steps" 149 | mobile: 150 | title: "📱 Mobile Setup" 151 | description: "Install WireGuard app and scan the QR code above." 152 | pc: 153 | title: "💻 PC Setup" 154 | description: "Install WireGuard client and import the configuration file." 155 | test: 156 | title: "🔗 Connection Test" 157 | description: "Activate the connection and verify VPN is working properly." 158 | manage: 159 | title: "🔧 Management" 160 | description: "Manage from client list if you encounter any issues." 161 | actions: 162 | create_another: "Create Another Client" 163 | view_clients: "View Client List" 164 | back_dashboard: "Back to Dashboard" 165 | 166 | clients: 167 | title: "Client List" 168 | subtitle: "Manage your registered WireGuard clients" 169 | new_client: "Create New Client" 170 | registered_clients: "Registered Clients" 171 | filter: "Filter:" 172 | all_interfaces: "All Interfaces" 173 | reset: "Reset" 174 | no_clients: 175 | title: "No Clients" 176 | description: "Create your first WireGuard client to get started." 177 | action: "Create First Client" 178 | status: 179 | active: "Active" 180 | waiting: "Waiting" 181 | fields: 182 | ip_address: "IP Address" 183 | public_key: "Public Key" 184 | keep_alive: "Keep Alive" 185 | last_activity: "Last Activity" 186 | never_connected: "Never Connected" 187 | not_set: "Not Set" 188 | seconds: "sec" 189 | delete_client: "Delete Client" 190 | delete_confirm: "Are you sure you want to delete \"%{name}\" client?\n\nThis action cannot be undone." 191 | stats: 192 | total: "Total %{count} clients" 193 | description: "Manage active clients" 194 | refresh: "Refresh" 195 | 196 | languages: 197 | ko: "한국어" 198 | en: "English" 199 | zh: "中文" 200 | ja: "日本語" -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The change you wanted was rejected (422 Unprocessable Entity) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /app/views/clients/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

7 | <%= t('clients.title') %> 8 |

9 |

<%= t('clients.subtitle') %>

10 |
11 |
12 | <%= link_to new_client_path(interface: @selected_interface), 13 | class: "group relative px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-md hover: transition-all duration-200" do %> 14 | 15 | 16 | <%= t('clients.new_client') %> 17 | 18 | <% end %> 19 |
20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 |

29 | 30 | <%= t('clients.registered_clients') %> 31 |

32 | 33 | 34 | <% if @wireguard_interfaces.any? %> 35 |
36 | <%= form_with url: clients_path, method: :get, local: true, class: "flex items-center space-x-3" do |f| %> 37 |
38 | 39 | 40 |
41 | <%= f.select :interface, 42 | options_for_select(@wireguard_interfaces.map { |iface| [iface[:name], iface[:name]] }, @selected_interface), 43 | { prompt: t('clients.all_interfaces') }, 44 | { 45 | class: "px-4 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white transition-all duration-200", 46 | onchange: "this.form.submit()" 47 | } %> 48 | <% if @selected_interface.present? %> 49 | <%= link_to t('clients.reset'), clients_path, 50 | class: "px-3 py-2 text-sm text-gray-600 hover:text-purple-600 font-medium transition-colors duration-200" %> 51 | <% end %> 52 | <% end %> 53 |
54 | <% end %> 55 |
56 |
57 | 58 | 59 | <% if @peers.empty? %> 60 |
61 |
62 |
63 | 64 |
65 |

<%= t('clients.no_clients.title') %>

66 |

<%= t('clients.no_clients.description') %>

67 | <%= link_to new_client_path(interface: @selected_interface), 68 | class: "group inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-md hover: transition-all duration-200" do %> 69 | 70 | <%= t('clients.no_clients.action') %> 71 | <% end %> 72 |
73 |
74 | <% else %> 75 | 76 |
77 |
78 | <% @peers.each do |peer| %> 79 |
80 | 81 |
82 |
83 |
84 |
85 | 86 |
87 |
88 |

89 | <%= peer[:name] || peer['.id'] %> 90 |

91 |

92 | <%= peer[:interface] %> 93 |

94 |
95 |
96 | 97 | 98 | <% if peer[:'last-handshake'].present? %> 99 |
100 |
101 | <%= t('clients.status.active') %> 102 |
103 | <% else %> 104 |
105 |
106 | <%= t('clients.status.waiting') %> 107 |
108 | <% end %> 109 |
110 |
111 | 112 | 113 |
114 | 115 |
116 |
117 | 118 | <%= t('clients.fields.ip_address') %> 119 |
120 | 121 | <%= peer[:'allowed-address'] %> 122 | 123 |
124 | 125 | 126 |
127 |
128 | 129 | <%= t('clients.fields.public_key') %> 130 |
131 | 132 | <%= truncate(peer[:'public-key'], length: 16) if peer[:'public-key'] %>... 133 | 134 |
135 | 136 | 137 |
138 |
139 | 140 | <%= t('clients.fields.keep_alive') %> 141 |
142 | 143 | <%= peer[:'persistent-keepalive']&.gsub("s", "") || t('clients.fields.not_set') %> 144 | <% if peer[:'persistent-keepalive'].present? %><%= t('clients.fields.seconds') %><% end %> 145 | 146 |
147 | 148 | 149 |
150 |
151 | 152 | <%= t('clients.fields.last_activity') %> 153 |
154 | 155 | <%= peer[:'last-handshake'] || t('clients.fields.never_connected') %> 156 | 157 |
158 |
159 | 160 | 161 |
162 | <%= button_to client_path(peer[:'.id']), 163 | method: :delete, 164 | data: { 165 | turbo_confirm: t('clients.delete_confirm', name: peer[:name] || peer[:'.id']), 166 | turbo_stream: true 167 | }, 168 | class: "w-full group flex items-center justify-center px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-medium rounded transition-all duration-200" do %> 169 | 170 | <%= t('clients.delete_client') %> 171 | <% end %> 172 |
173 |
174 | <% end %> 175 |
176 | 177 | 178 |
179 |
180 |
181 | 182 | 183 | <%= t('clients.stats.total', count: @peers.count) %> 184 | 185 |
186 |

<%= t('clients.stats.description') %>

187 |
188 | <%= link_to clients_path(interface: @selected_interface), 189 | class: "group inline-flex items-center px-6 py-3 bg-white hover:bg-gray-50 border border-gray-300 hover:border-gray-400 text-gray-700 hover:text-purple-600 font-semibold rounded-md transition-all duration-200 " do %> 190 | 191 | <%= t('clients.refresh') %> 192 | <% end %> 193 |
194 |
195 | <% end %> 196 |
197 |
198 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @source "./safelist.txt"; 3 | 4 | @theme { 5 | --color-wg-primary: rgb(59, 130, 246); 6 | --color-wg-primary-dark: rgb(37, 99, 235); 7 | --color-wg-secondary: rgb(139, 92, 246); 8 | --color-wg-accent: rgb(236, 72, 153); 9 | --color-wg-success: rgb(34, 197, 94); 10 | --color-wg-danger: rgb(239, 68, 68); 11 | --color-wg-danger-dark: rgb(220, 38, 38); 12 | --color-wg-gray: rgb(107, 114, 128); 13 | --color-wg-dark: rgb(30, 41, 59); 14 | --color-wg-surface-light: rgba(255, 255, 255, 0.8); 15 | --color-wg-border: rgb(226, 232, 240); 16 | --color-wg-border-light: rgba(226, 232, 240, 0.5); 17 | } 18 | 19 | @layer base { 20 | body { 21 | min-height: 100vh; 22 | font-family: system-ui, -apple-system, sans-serif; 23 | color: var(--color-wg-dark); 24 | } 25 | 26 | hr { 27 | border-color: var(--color-wg-border); 28 | } 29 | 30 | .disabled { 31 | pointer-events: none; 32 | cursor: not-allowed; 33 | opacity: 0.5; 34 | } 35 | 36 | /* WireGuard 특화 컴포넌트 */ 37 | .wg-header { 38 | background: var(--color-wg-dark); 39 | backdrop-filter: blur(12px); 40 | border-bottom: 1px solid var(--color-wg-border-light); 41 | position: sticky; 42 | top: 0; 43 | z-index: 40; 44 | } 45 | 46 | .wg-container { 47 | max-width: 80rem; 48 | margin: 0 auto; 49 | padding: 0 1rem; 50 | } 51 | 52 | .wg-header-content { 53 | display: flex; 54 | justify-content: space-between; 55 | align-items: center; 56 | padding: 1rem 0; 57 | } 58 | 59 | .wg-logo { 60 | display: flex; 61 | align-items: center; 62 | gap: 0.75rem; 63 | } 64 | 65 | .wg-logo-icon { 66 | width: 2.5rem; 67 | height: 2.5rem; 68 | background: linear-gradient(to right, var(--color-wg-primary), var(--color-wg-secondary)); 69 | border-radius: 0.5rem; 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | } 74 | 75 | .wg-logo-text { 76 | font-size: 1.25rem; 77 | font-weight: 700; 78 | background: linear-gradient(to right, var(--color-wg-primary), var(--color-wg-secondary)); 79 | -webkit-background-clip: text; 80 | -webkit-text-fill-color: transparent; 81 | background-clip: text; 82 | } 83 | 84 | .wg-user-info { 85 | display: none; 86 | align-items: center; 87 | gap: 0.5rem; 88 | padding: 0.5rem 0.75rem; 89 | background: linear-gradient(to right, rgb(249, 250, 251), rgb(243, 244, 246)); 90 | border-radius: 9999px; 91 | border: 1px solid rgb(209, 213, 219); 92 | } 93 | 94 | .wg-status-indicator { 95 | width: 0.5rem; 96 | height: 0.5rem; 97 | background: var(--color-wg-success); 98 | border-radius: 50%; 99 | animation: pulse 2s infinite; 100 | } 101 | 102 | /* 버튼 스타일 */ 103 | .wg-btn-primary { 104 | display: inline-flex; 105 | align-items: center; 106 | justify-content: center; 107 | padding: 0.5rem 1rem; 108 | border-radius: 0.375rem; 109 | font-weight: 500; 110 | transition: all 0.2s ease; 111 | cursor: pointer; 112 | background: linear-gradient(to right, var(--color-wg-primary), var(--color-wg-primary-dark)); 113 | color: white; 114 | } 115 | 116 | .wg-btn-primary:hover { 117 | } 118 | 119 | .wg-btn-danger { 120 | display: inline-flex; 121 | align-items: center; 122 | justify-content: center; 123 | padding: 0.5rem 1rem; 124 | border-radius: 0.375rem; 125 | font-weight: 500; 126 | transition: all 0.2s ease; 127 | cursor: pointer; 128 | background: linear-gradient(to right, var(--color-wg-danger), var(--color-wg-danger-dark)); 129 | color: white; 130 | } 131 | 132 | .wg-btn-danger:hover { 133 | } 134 | 135 | /* 폼 스타일 */ 136 | .wg-form-group { 137 | display: flex; 138 | flex-direction: column; 139 | gap: 0.5rem; 140 | } 141 | 142 | .wg-label { 143 | display: block; 144 | font-size: 0.875rem; 145 | font-weight: 600; 146 | color: var(--color-wg-dark); 147 | } 148 | 149 | .wg-input { 150 | width: 100%; 151 | padding: 0.75rem 1rem; 152 | border: 2px solid var(--color-wg-border); 153 | border-radius: 0.375rem; 154 | outline: none; 155 | transition: all 0.2s ease; 156 | background: white; 157 | } 158 | 159 | .wg-input:focus { 160 | border-color: var(--color-wg-primary); 161 | } 162 | 163 | 164 | 165 | @media (min-width: 640px) { 166 | .wg-container { 167 | padding: 0 1.5rem; 168 | } 169 | 170 | .wg-user-info { 171 | display: flex; 172 | } 173 | } 174 | 175 | @media (min-width: 1024px) { 176 | .wg-container { 177 | padding: 0 2rem; 178 | } 179 | } 180 | } 181 | 182 | @layer utilities { 183 | /* 대시보드 스타일 */ 184 | .wg-dashboard-header { 185 | margin-bottom: 2rem; 186 | text-align: center; 187 | } 188 | 189 | .wg-dashboard-title { 190 | font-size: 2.5rem; 191 | font-weight: 700; 192 | background: linear-gradient(to right, var(--color-wg-primary), var(--color-wg-secondary)); 193 | -webkit-background-clip: text; 194 | -webkit-text-fill-color: transparent; 195 | background-clip: text; 196 | margin-bottom: 0.75rem; 197 | } 198 | 199 | .wg-dashboard-subtitle { 200 | color: var(--color-wg-gray); 201 | font-size: 1.125rem; 202 | } 203 | 204 | .wg-dashboard-grid { 205 | display: grid; 206 | grid-template-columns: 1fr; 207 | gap: 2rem; 208 | margin-bottom: 2rem; 209 | } 210 | 211 | .wg-dashboard-card-primary { 212 | display: block; 213 | background: linear-gradient(135deg, var(--color-wg-primary), var(--color-wg-primary-dark)); 214 | color: white; 215 | border-radius: 0.5rem; 216 | transition: all 0.3s ease; 217 | border: 1px solid rgb(209, 213, 219); 218 | } 219 | 220 | .wg-dashboard-card-primary:hover { 221 | 222 | } 223 | 224 | .wg-dashboard-card-secondary { 225 | display: block; 226 | background: linear-gradient(135deg, var(--color-wg-secondary), var(--color-wg-accent)); 227 | color: white; 228 | border-radius: 0.5rem; 229 | transition: all 0.3s ease; 230 | border: 1px solid rgb(209, 213, 219); 231 | } 232 | 233 | .wg-dashboard-card-secondary:hover { 234 | 235 | } 236 | 237 | /* 로그인 페이지 스타일 */ 238 | .wg-login-container { 239 | max-width: 40rem; 240 | margin: 0 auto; 241 | } 242 | 243 | .wg-login-card { 244 | background: var(--color-wg-surface-light); 245 | backdrop-filter: blur(16px); 246 | border-radius: 0.5rem; 247 | border: 1px solid rgb(209, 213, 219); 248 | overflow: hidden; 249 | padding: 2rem; 250 | transition: all 0.3s ease; 251 | } 252 | 253 | .wg-login-header { 254 | text-align: center; 255 | margin-bottom: 2rem; 256 | } 257 | 258 | .wg-login-icon { 259 | width: 4rem; 260 | height: 4rem; 261 | background: linear-gradient(to right, var(--color-wg-primary), var(--color-wg-secondary)); 262 | border-radius: 0.5rem; 263 | display: flex; 264 | align-items: center; 265 | justify-content: center; 266 | margin: 0 auto 1rem; 267 | } 268 | 269 | .wg-login-title { 270 | font-size: 1.875rem; 271 | font-weight: 700; 272 | background: linear-gradient(to right, var(--color-wg-dark), var(--color-wg-gray)); 273 | -webkit-background-clip: text; 274 | -webkit-text-fill-color: transparent; 275 | background-clip: text; 276 | margin-bottom: 0.5rem; 277 | } 278 | 279 | .wg-login-subtitle { 280 | color: var(--color-wg-gray); 281 | font-size: 0.875rem; 282 | } 283 | 284 | /* 연결 정보 스타일 */ 285 | .wg-connection-info { 286 | background: var(--color-wg-surface-light); 287 | backdrop-filter: blur(16px); 288 | border-radius: 0.5rem; 289 | border: 1px solid rgb(209, 213, 219); 290 | padding: 2rem; 291 | margin-bottom: 2rem; 292 | } 293 | 294 | .wg-connection-header { 295 | display: flex; 296 | align-items: center; 297 | justify-content: space-between; 298 | margin-bottom: 1.5rem; 299 | } 300 | 301 | .wg-connection-title { 302 | font-size: 1.25rem; 303 | font-weight: 700; 304 | color: var(--color-wg-dark); 305 | display: flex; 306 | align-items: center; 307 | gap: 0.75rem; 308 | } 309 | 310 | .wg-connection-status { 311 | display: flex; 312 | align-items: center; 313 | gap: 0.5rem; 314 | } 315 | 316 | .wg-connection-grid { 317 | display: grid; 318 | grid-template-columns: 1fr; 319 | gap: 1.5rem; 320 | } 321 | 322 | .wg-connection-item { 323 | background: linear-gradient(135deg, rgb(249, 250, 251), rgb(243, 244, 246)); 324 | border-radius: 0.375rem; 325 | padding: 1rem; 326 | border: 1px solid rgb(209, 213, 219); 327 | } 328 | 329 | .wg-connection-item-header { 330 | display: flex; 331 | align-items: center; 332 | gap: 0.75rem; 333 | margin-bottom: 0.5rem; 334 | } 335 | 336 | .wg-connection-item-value { 337 | font-size: 1.125rem; 338 | font-weight: 700; 339 | color: var(--color-wg-dark); 340 | } 341 | 342 | @media (min-width: 768px) { 343 | .wg-dashboard-grid { 344 | grid-template-columns: repeat(2, 1fr); 345 | } 346 | 347 | .wg-connection-grid { 348 | grid-template-columns: repeat(3, 1fr); 349 | } 350 | } 351 | } 352 | 353 | @media (min-width: 640px) { 354 | .wg-dashboard-title { 355 | font-size: 3rem; 356 | } 357 | 358 | .wg-dashboard-subtitle { 359 | font-size: 1.25rem; 360 | } 361 | } 362 | 363 | .wg-input::-webkit-outer-spin-button, 364 | .wg-input::-webkit-inner-spin-button { 365 | -webkit-appearance: none; 366 | margin: 0; 367 | } 368 | 369 | .wg-input[type="number"] { 370 | -moz-appearance: textfield; 371 | } 372 | 373 | /* 폰트 */ 374 | @font-face { 375 | font-family: 'Pretendard-Regular'; 376 | src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff'); 377 | font-weight: 400; 378 | font-style: normal; 379 | } 380 | 381 | 382 | /* 장식적 요소 */ 383 | .wg-decorative-bg { 384 | position: absolute; 385 | inset: 0; 386 | z-index: -10; 387 | overflow: hidden; 388 | } 389 | 390 | .wg-decorative-bg::before { 391 | content: ''; 392 | position: absolute; 393 | top: -10rem; 394 | right: -10rem; 395 | width: 20rem; 396 | height: 20rem; 397 | background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); 398 | border-radius: 50%; 399 | filter: blur(3rem); 400 | } 401 | 402 | .wg-decorative-bg::after { 403 | content: ''; 404 | position: absolute; 405 | bottom: -10rem; 406 | left: -10rem; 407 | width: 20rem; 408 | height: 20rem; 409 | background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(236, 72, 153, 0.1) 100%); 410 | border-radius: 50%; 411 | filter: blur(3rem); 412 | } 413 | --------------------------------------------------------------------------------