26 |
--------------------------------------------------------------------------------
/graph-sample/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-*
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | # Ignore pidfiles, but keep the directory.
21 | /tmp/pids/*
22 | !/tmp/pids/
23 | !/tmp/pids/.keep
24 |
25 | # Ignore uploaded files in development.
26 | /storage/*
27 | !/storage/.keep
28 | /tmp/storage/*
29 | !/tmp/storage/
30 | !/tmp/storage/.keep
31 |
32 | /public/assets
33 |
34 | # Ignore master key for decrypting credentials and more.
35 | /config/master.key
36 |
--------------------------------------------------------------------------------
/graph-sample/config/application.rb:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft Corporation.
2 | # Licensed under the MIT License.
3 | # frozen_string_literal: true
4 |
5 | require_relative "boot"
6 |
7 | require "rails/all"
8 |
9 | # Require the gems listed in Gemfile, including any gems
10 | # you've limited to :test, :development, or :production.
11 | Bundler.require(*Rails.groups)
12 |
13 | module GraphSample
14 | class Application < Rails::Application
15 | # Initialize configuration defaults for originally generated Rails version.
16 | config.load_defaults 7.0
17 |
18 | # Configuration for the application, engines, and railties goes here.
19 | #
20 | # These settings can be overridden in specific environments using the files
21 | # in config/environments, which are processed later.
22 | #
23 | # config.time_zone = "Central Time (US & Canada)"
24 | # config.eager_load_paths << Rails.root.join("extras")
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug report, needs triage
6 | assignees: ''
7 | ---
8 |
9 | ### Describe the bug
10 |
11 | A clear and concise description of what the bug is.
12 |
13 | ### To Reproduce
14 |
15 | Steps to reproduce the behavior:
16 |
17 | 1. Go to '...'
18 | 1. Click on '....'
19 | 1. Scroll down to '....'
20 | 1. See error
21 |
22 | ### Expected behavior
23 |
24 | A clear and concise description of what you expected to happen.
25 |
26 | ### Screenshots
27 |
28 | If applicable, add screenshots to help explain your problem.
29 |
30 | ### Desktop
31 |
32 | - OS: [e.g. iOS]
33 | - Browser [e.g. chrome, safari]
34 | - Version [e.g. 22]
35 |
36 | ### Dependency versions
37 |
38 | - Authentication library (MSAL, etc.) version:
39 | - Graph library (Graph SDK, REST library, etc.) version:
40 |
41 | ### Additional context
42 |
43 | Add any other context about the problem here.
44 |
--------------------------------------------------------------------------------
/graph-sample/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/graph-sample/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
6 | * vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
17 | .alert-pre {
18 | word-wrap: break-word;
19 | word-break: break-all;
20 | white-space: pre-wrap;
21 | }
22 |
23 | .external-link {
24 | padding-top: 6px;
25 | }
26 |
27 | .avatar-link {
28 | padding-top: 4px;
29 | padding-bottom: 4px;
30 | }
31 |
32 | .profile-photo {
33 | width: 32px;
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Microsoft Graph
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/graph-sample/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/graph-sample/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[7.2].define(version: 2022_10_04_165905) do
14 | create_table "sessions", force: :cascade do |t|
15 | t.string "session_id", null: false
16 | t.text "data"
17 | t.datetime "created_at", null: false
18 | t.datetime "updated_at", null: false
19 | t.index ["session_id"], name: "index_sessions_on_session_id", unique: true
20 | t.index ["updated_at"], name: "index_sessions_on_updated_at"
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/graph-sample/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Define an application-wide content security policy.
6 | # See the Securing Rails Applications Guide for more information:
7 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
8 |
9 | # Rails.application.configure do
10 | # config.content_security_policy do |policy|
11 | # policy.default_src :self, :https
12 | # policy.font_src :self, :https, :data
13 | # policy.img_src :self, :https, :data
14 | # policy.object_src :none
15 | # policy.script_src :self, :https
16 | # policy.style_src :self, :https
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 | #
21 | # # Generate session nonces for permitted importmap and inline scripts
22 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
23 | # config.content_security_policy_nonce_directives = %w(script-src)
24 | #
25 | # # Report violations without enforcing the policy.
26 | # # config.content_security_policy_report_only = true
27 | # end
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rbc
2 | capybara-*.html
3 | .rspec
4 | /log
5 | /tmp
6 | /db/*.sqlite3
7 | /db/*.sqlite3-journal
8 | /public/system
9 | /coverage/
10 | /spec/tmp
11 | *.orig
12 | rerun.txt
13 | pickle-email-*.html
14 |
15 | # TODO Comment out this rule if you are OK with secrets being uploaded to the repo
16 | config/initializers/secret_token.rb
17 | config/master.key
18 |
19 | # Only include if you have production secrets in this file, which is no longer a Rails default
20 | # config/secrets.yml
21 |
22 | # dotenv
23 | # TODO Comment out this rule if environment variables can be committed
24 | .env
25 |
26 | ## Environment normalization:
27 | /.bundle
28 | /vendor/bundle
29 |
30 | # these should all be checked in to normalize the environment:
31 | # Gemfile.lock, .ruby-version, .ruby-gemset
32 |
33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34 | .rvmrc
35 |
36 | # if using bower-rails ignore default bower_components path bower.json files
37 | /vendor/assets/bower_components
38 | *.bowerrc
39 | bower.json
40 |
41 | # Ignore pow environment settings
42 | .powenv
43 |
44 | # Ignore Byebug command history file.
45 | .byebug_history
46 |
47 | # Ignore node_modules
48 | node_modules/
49 |
50 | # Visual Studio Code
51 | .vscode/
52 |
53 | oauth_environment_variables.rb
54 |
--------------------------------------------------------------------------------
/graph-sample/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 |
--------------------------------------------------------------------------------
/graph-sample/app/views/calendar/new.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/graph-sample/config/puma.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Puma can serve each request in a thread from an internal thread pool.
4 | # The `threads` method setting takes two numbers: a minimum and maximum.
5 | # Any libraries that use thread pools should be configured to match
6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
7 | # and maximum; this matches the default thread size of Active Record.
8 | #
9 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
10 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
11 | threads min_threads_count, max_threads_count
12 |
13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
14 | # terminating a worker in development environments.
15 | #
16 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
17 |
18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
19 | #
20 | port ENV.fetch("PORT") { 3000 }
21 |
22 | # Specifies the `environment` that Puma will run in.
23 | #
24 | environment ENV.fetch("RAILS_ENV") { "development" }
25 |
26 | # Specifies the `pidfile` that Puma will use.
27 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
28 |
29 | # Specifies the number of `workers` to boot in clustered mode.
30 | # Workers are forked web server processes. If using threads and workers together
31 | # the concurrency of the application would be max `threads` * `workers`.
32 | # Workers do not work on JRuby or Windows (both of which do not support
33 | # processes).
34 | #
35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
36 |
37 | # Use the `preload_app!` method when specifying a `workers` number.
38 | # This directive tells Puma to first boot the application and load code
39 | # before forking the application. This takes advantage of Copy On Write
40 | # process behavior so workers use less memory.
41 | #
42 | # preload_app!
43 |
44 | # Allow puma to be restarted by `bin/rails restart` command.
45 | plugin :tmp_restart
46 |
--------------------------------------------------------------------------------
/graph-sample/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/graph-sample/lib/microsoft_graph_auth.rb:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft Corporation.
2 | # Licensed under the MIT License.
3 | # frozen_string_literal: true
4 |
5 | require 'omniauth-oauth2'
6 |
7 | module OmniAuth
8 | module Strategies
9 | # Implements an OmniAuth strategy to get a Microsoft Graph
10 | # compatible token from Azure AD
11 | class MicrosoftGraphAuth < OmniAuth::Strategies::OAuth2
12 | option :name, :microsoft_graph_auth
13 |
14 | DEFAULT_SCOPE = 'openid email profile User.Read'
15 |
16 | # Configure the Microsoft identity platform endpoints
17 | option :client_options,
18 | site: 'https://login.microsoftonline.com',
19 | authorize_url: '/common/oauth2/v2.0/authorize',
20 | token_url: '/common/oauth2/v2.0/token'
21 |
22 | option :pcke, true
23 | # Send the scope parameter during authorize
24 | option :authorize_options, [:scope]
25 |
26 | # Unique ID for the user is the id field
27 | uid { raw_info['id'] }
28 |
29 | # Get additional information after token is retrieved
30 | extra do
31 | {
32 | 'raw_info' => raw_info
33 | }
34 | end
35 |
36 | def raw_info
37 | # Get user profile information from the /me endpoint
38 | @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me?$select=displayName,mail,mailboxSettings,userPrincipalName').parsed
39 | end
40 |
41 | def authorize_params
42 | super.tap do |params|
43 | params[:scope] = request.params['scope'] if request.params['scope']
44 | params[:scope] ||= DEFAULT_SCOPE
45 | end
46 | end
47 |
48 | # Override callback URL
49 | # OmniAuth by default passes the entire URL of the callback, including
50 | # query parameters. Azure fails validation because that doesn't match the
51 | # registered callback.
52 | def callback_url
53 | options[:redirect_uri] || (full_host + script_name + callback_path)
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/graph-sample/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft Corporation.
2 | # Licensed under the MIT License.
3 | # frozen_string_literal: true
4 |
5 | require 'microsoft_graph_auth'
6 | require 'oauth2'
7 |
8 | class ApplicationController < ActionController::Base
9 | before_action :set_user
10 |
11 | def set_user
12 | @user_name = user_name
13 | @user_email = user_email
14 | end
15 |
16 | def save_in_session(auth_hash)
17 | # Save the token info
18 | session[:graph_token_hash] = auth_hash[:credentials]
19 | # Save the user's display name
20 | session[:user_name] = auth_hash.dig(:extra, :raw_info, :displayName)
21 | # Save the user's email address
22 | # Use the mail field first. If that's empty, fall back on
23 | # userPrincipalName
24 | session[:user_email] = auth_hash.dig(:extra, :raw_info, :mail) ||
25 | auth_hash.dig(:extra, :raw_info, :userPrincipalName)
26 | # Save the user's time zone
27 | session[:user_timezone] = auth_hash.dig(:extra, :raw_info, :mailboxSettings, :timeZone)
28 | end
29 |
30 | def user_name
31 | session[:user_name]
32 | end
33 |
34 | def user_email
35 | session[:user_email]
36 | end
37 |
38 | def user_timezone
39 | session[:user_timezone]
40 | end
41 |
42 | def access_token
43 | token_hash = session[:graph_token_hash]
44 | return if token_hash.nil?
45 |
46 | # Get the expiry time - 5 minutes
47 | expiry = Time.at(token_hash[:expires_at] - 300)
48 |
49 | if Time.now > expiry
50 | # Token expired, refresh
51 | new_hash = refresh_tokens token_hash
52 | new_hash[:token]
53 | else
54 | token_hash[:token]
55 | end
56 | end
57 |
58 | def refresh_tokens(token_hash)
59 | oauth_strategy = OmniAuth::Strategies::MicrosoftGraphAuth.new(
60 | nil, ENV.fetch('AZURE_APP_ID'), ENV.fetch('AZURE_APP_SECRET')
61 | )
62 |
63 | token = OAuth2::AccessToken.new(
64 | oauth_strategy.client, token_hash[:token],
65 | refresh_token: token_hash[:refresh_token]
66 | )
67 |
68 | # Refresh the tokens
69 | new_tokens = token.refresh!.to_hash.slice(:access_token, :refresh_token, :expires_at)
70 |
71 | # Rename token key
72 | new_tokens[:token] = new_tokens.delete :access_token
73 |
74 | # Store the new hash
75 | session[:graph_token_hash] = new_tokens
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/graph-sample/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 |
46 | # Print deprecation notices to the stderr.
47 | config.active_support.deprecation = :stderr
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raises error for missing translations.
56 | # config.i18n.raise_on_missing_translations = true
57 |
58 | # Annotate rendered view with file names.
59 | # config.action_view.annotate_rendered_view_with_filenames = true
60 | end
61 |
--------------------------------------------------------------------------------
/graph-sample/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 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Store uploaded files on the local file system (see config/storage.yml for options).
37 | config.active_storage.service = :local
38 |
39 | # Don't care if the mailer can't send.
40 | config.action_mailer.raise_delivery_errors = false
41 |
42 | config.action_mailer.perform_caching = false
43 |
44 | # Print deprecation notices to the Rails logger.
45 | config.active_support.deprecation = :log
46 |
47 | # Raise exceptions for disallowed deprecations.
48 | config.active_support.disallowed_deprecation = :raise
49 |
50 | # Tell Active Support which deprecation messages to disallow.
51 | config.active_support.disallowed_deprecation_warnings = []
52 |
53 | # Raise an error on page load if there are pending migrations.
54 | config.active_record.migration_error = :page_load
55 |
56 | # Highlight code that triggered database queries in logs.
57 | config.active_record.verbose_query_logs = true
58 |
59 | # Suppress logger output for asset requests.
60 | config.assets.quiet = true
61 |
62 | # Raises error for missing translations.
63 | # config.i18n.raise_on_missing_translations = true
64 |
65 | # Annotate rendered view with file names.
66 | # config.action_view.annotate_rendered_view_with_filenames = true
67 |
68 | # Uncomment if you wish to allow Action Cable access from any origin.
69 | # config.action_cable.disable_request_forgery_protection = true
70 | end
71 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/graph-sample/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby "3.3.5"
5 |
6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
7 | gem "rails", "~> 7.2.2"
8 |
9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
10 | gem "sprockets-rails"
11 |
12 | # Use sqlite3 as the database for Active Record
13 | gem "sqlite3", "~> 2.6"
14 |
15 | # Use the Puma web server [https://github.com/puma/puma]
16 | gem "puma", "~> 6.6"
17 |
18 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
19 | gem "importmap-rails"
20 |
21 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
22 | gem "turbo-rails"
23 |
24 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
25 | gem "stimulus-rails"
26 |
27 | # Build JSON APIs with ease [https://github.com/rails/jbuilder]
28 | gem "jbuilder"
29 |
30 | # Use Redis adapter to run Action Cable in production
31 | # gem "redis", "~> 4.0"
32 |
33 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
34 | # gem "kredis"
35 |
36 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
37 | # gem "bcrypt", "~> 3.1.7"
38 |
39 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
40 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
41 |
42 | # Reduces boot times through caching; required in config/boot.rb
43 | gem "bootsnap", require: false
44 |
45 | # Use Sass to process CSS
46 | # gem "sassc-rails"
47 |
48 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
49 | # gem "image_processing", "~> 1.2"
50 |
51 | group :development, :test do
52 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
53 | gem "debug", platforms: %i[ mri mingw x64_mingw ]
54 | end
55 |
56 | group :development do
57 | # Use console on exceptions pages [https://github.com/rails/web-console]
58 | gem "web-console"
59 |
60 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
61 | # gem "rack-mini-profiler"
62 |
63 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
64 | # gem "spring"
65 | end
66 |
67 | group :test do
68 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
69 | gem "capybara"
70 | gem "selenium-webdriver"
71 | gem "webdrivers"
72 | end
73 |
74 | # OAuth
75 | gem 'omniauth-oauth2', '~> 1.8.0'
76 | # OmniAuth CSRF protection
77 | gem 'omniauth-rails_csrf_protection', '~> 1.0.2'
78 | # REST calls to Microsoft Graph
79 | gem 'httparty', '~> 0.23.1'
80 | # Session storage in database
81 | gem 'activerecord-session_store', '~> 2.2.0'
82 |
83 | gem "ostruct", "~> 0.6.1"
84 |
--------------------------------------------------------------------------------
/graph-sample/.rubocop_todo.yml:
--------------------------------------------------------------------------------
1 | # This configuration was generated by
2 | # `rubocop --auto-gen-config`
3 | # on 2022-10-04 20:43:23 UTC using RuboCop version 1.36.0.
4 | # The point is for the user to remove these configuration records
5 | # one by one as the offenses are removed from the code base.
6 | # Note that changes in the inspected code, or installation of new
7 | # versions of RuboCop, may require this file to be generated again.
8 |
9 | # Offense count: 2
10 | # This cop supports safe autocorrection (--autocorrect).
11 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
12 | # SupportedStyles: space, no_space, compact
13 | # SupportedStylesForEmptyBrackets: space, no_space
14 | Layout/SpaceInsideArrayLiteralBrackets:
15 | Exclude:
16 | - 'config/environments/production.rb'
17 |
18 | # Offense count: 4
19 | # This cop supports safe autocorrection (--autocorrect).
20 | Layout/SpaceInsidePercentLiteralDelimiters:
21 | Exclude:
22 | - 'Gemfile'
23 |
24 | # Offense count: 5
25 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods.
26 | Metrics/MethodLength:
27 | Max: 30
28 |
29 | # Offense count: 1
30 | # Configuration parameters: CountComments, CountAsOne.
31 | Metrics/ModuleLength:
32 | Max: 212
33 |
34 | # Offense count: 2
35 | # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
36 | Metrics/ParameterLists:
37 | Max: 7
38 |
39 | # Offense count: 8
40 | # Configuration parameters: AllowedConstants.
41 | Style/Documentation:
42 | Exclude:
43 | - 'spec/**/*'
44 | - 'test/**/*'
45 | - 'app/controllers/application_controller.rb'
46 | - 'app/controllers/auth_controller.rb'
47 | - 'app/controllers/home_controller.rb'
48 | - 'app/helpers/application_helper.rb'
49 | - 'app/helpers/auth_helper.rb'
50 | - 'app/helpers/calendar_helper.rb'
51 | - 'app/helpers/home_helper.rb'
52 | - 'config/application.rb'
53 |
54 | # Offense count: 1
55 | # This cop supports unsafe autocorrection (--autocorrect-all).
56 | Style/GlobalStdStream:
57 | Exclude:
58 | - 'config/environments/production.rb'
59 |
60 | # Offense count: 2
61 | # This cop supports unsafe autocorrection (--autocorrect-all).
62 | # Configuration parameters: SafeForConstants.
63 | Style/RedundantFetchBlock:
64 | Exclude:
65 | - 'config/puma.rb'
66 |
67 | # Offense count: 60
68 | # This cop supports safe autocorrection (--autocorrect).
69 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
70 | # SupportedStyles: single_quotes, double_quotes
71 | Style/StringLiterals:
72 | Exclude:
73 | - 'Gemfile'
74 | - 'Rakefile'
75 | - 'app/mailers/application_mailer.rb'
76 | - 'config.ru'
77 | - 'config/application.rb'
78 | - 'config/boot.rb'
79 | - 'config/environment.rb'
80 | - 'config/environments/development.rb'
81 | - 'config/environments/production.rb'
82 | - 'config/environments/test.rb'
83 | - 'config/importmap.rb'
84 | - 'config/initializers/assets.rb'
85 | - 'config/puma.rb'
86 |
87 | # Offense count: 2
88 | # This cop supports safe autocorrection (--autocorrect).
89 | # Configuration parameters: .
90 | # SupportedStyles: percent, brackets
91 | Style/SymbolArray:
92 | EnforcedStyle: percent
93 | MinSize: 10
94 |
--------------------------------------------------------------------------------
/graph-sample/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 =~ 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", __FILE__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
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 || cli_arg_version ||
66 | bundler_requirement_for(lockfile_version)
67 | end
68 |
69 | def bundler_requirement_for(version)
70 | return "#{Gem::Requirement.default}.a" unless version
71 |
72 | bundler_gem_version = Gem::Version.new(version)
73 |
74 | requirement = bundler_gem_version.approximate_recommendation
75 |
76 | return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0")
77 |
78 | requirement += ".a" if bundler_gem_version.prerelease?
79 |
80 | requirement
81 | end
82 |
83 | def load_bundler!
84 | ENV["BUNDLE_GEMFILE"] ||= gemfile
85 |
86 | activate_bundler
87 | end
88 |
89 | def activate_bundler
90 | gem_error = activation_error_handling do
91 | gem "bundler", bundler_requirement
92 | end
93 | return if gem_error.nil?
94 | require_error = activation_error_handling do
95 | require "bundler/version"
96 | end
97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
98 | 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}'`"
99 | exit 42
100 | end
101 |
102 | def activation_error_handling
103 | yield
104 | nil
105 | rescue StandardError, LoadError => e
106 | e
107 | end
108 | end
109 |
110 | m.load_bundler!
111 |
112 | if m.invoked_as_script?
113 | load Gem.bin_path("bundler", "bundle")
114 | end
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_type: sample
3 | description: This sample demonstrates how to use the Microsoft Graph REST API to access data in Office 365 from Ruby on Rails apps.
4 | products:
5 | - ms-graph
6 | - office-exchange-online
7 | languages:
8 | - ruby
9 | ---
10 |
11 | # Microsoft Graph sample Ruby on Rails app
12 |
13 | [](https://github.com/microsoftgraph/msgraph-sample-rubyrailsapp/actions/workflows/ruby.yml) 
14 |
15 | This sample demonstrates how to use the Microsoft Graph REST API to access data in Office 365 from Ruby on Rails apps.
16 |
17 | > **NOTE:** This sample was originally built from a tutorial published on the [Microsoft Graph tutorials](https://learn.microsoft.com/graph/tutorials) page. That tutorial has been removed.
18 |
19 | ## Prerequisites
20 |
21 | To run the completed project in this folder, you need the following:
22 |
23 | - [Ruby](https://www.ruby-lang.org/en/downloads/)
24 | - [SQLite3](https://sqlite.org/index.html)
25 |
26 | This sample was written for Ruby 3.1.2.
27 |
28 | ## Register a web application with the Microsoft Entra admin center
29 |
30 | 1. Open a browser and navigate to the [Microsoft Entra admin center](https://entra.microsoft.com). Login using a **Work or School Account**.
31 |
32 | 1. Expand **Identity** in the left-hand navigation, expand **Applications**, then select **App registrations**.
33 |
34 | 1. Select **New registration**. On the **Register an application** page, set the values as follows.
35 |
36 | - Set **Name** to `Ruby Graph Tutorial`.
37 | - Set **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts**.
38 | - Under **Redirect URI**, set the first drop-down to `Web` and set the value to `http://localhost:3000/auth/microsoft_graph_auth/callback`.
39 |
40 | 1. Choose **Register**. On the **Ruby Graph Tutorial** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step.
41 |
42 | 1. Select **Certificates & secrets** under **Manage**. Select the **New client secret** button. Enter a value in **Description** and select one of the options for **Expires** and choose **Add**.
43 |
44 | 1. Copy the client secret value before you leave this page. You will need it in the next step.
45 |
46 | ## Configure the sample
47 |
48 | 1. Rename the `./graph-sample/config/oauth_environment_variables.rb.example` file to `oauth_environment_variables.rb`.
49 |
50 | 1. Edit the `oauth_environment_variables.rb` file and make the following changes.
51 | 1. Replace `YOUR_APP_ID_HERE` with the **Application Id** you got from the App Registration Portal.
52 | 1. Replace `YOUR_APP_SECRET_HERE` with the secret you got from the App Registration Portal.
53 |
54 | 1. In your command-line interface (CLI), navigate to the `./graph-sample` directory and run the following command to install requirements.
55 |
56 | ```Shell
57 | bundle install
58 | ```
59 |
60 | 1. In your CLI, run the following command to initialize the app's database.
61 |
62 | ```Shell
63 | rake db:migrate
64 | ```
65 |
66 | ## Run the sample
67 |
68 | 1. Run the following command in your CLI to start the application.
69 |
70 | ```Shell
71 | rails server
72 | ```
73 |
74 | 1. Open a browser and browse to `http://localhost:3000`.
75 |
76 | ## Code of conduct
77 |
78 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
79 |
80 | ## Disclaimer
81 |
82 | **THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
83 |
--------------------------------------------------------------------------------
/graph-sample/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.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Store uploaded files on the local file system (see config/storage.yml for options).
41 | config.active_storage.service = :local
42 |
43 | # Mount Action Cable outside main process or domain.
44 | # config.action_cable.mount_path = nil
45 | # config.action_cable.url = "wss://example.com/cable"
46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
47 |
48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
49 | # config.force_ssl = true
50 |
51 | # Include generic and useful information about system operation, but avoid logging too much
52 | # information to avoid inadvertent exposure of personally identifiable information (PII).
53 | config.log_level = :info
54 |
55 | # Prepend all log lines with the following tags.
56 | config.log_tags = [ :request_id ]
57 |
58 | # Use a different cache store in production.
59 | # config.cache_store = :mem_cache_store
60 |
61 | # Use a real queuing backend for Active Job (and separate queues per environment).
62 | # config.active_job.queue_adapter = :resque
63 | # config.active_job.queue_name_prefix = "graph_sample_production"
64 |
65 | config.action_mailer.perform_caching = false
66 |
67 | # Ignore bad email addresses and do not raise email delivery errors.
68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
69 | # config.action_mailer.raise_delivery_errors = false
70 |
71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
72 | # the I18n.default_locale when a translation cannot be found).
73 | config.i18n.fallbacks = true
74 |
75 | # Don't log any deprecations.
76 | config.active_support.report_deprecations = false
77 |
78 | # Use default logging formatter so that PID and timestamp are not suppressed.
79 | config.log_formatter = ::Logger::Formatter.new
80 |
81 | # Use a different logger for distributed setups.
82 | # require "syslog/logger"
83 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
84 |
85 | if ENV["RAILS_LOG_TO_STDOUT"].present?
86 | logger = ActiveSupport::Logger.new(STDOUT)
87 | logger.formatter = config.log_formatter
88 | config.logger = ActiveSupport::TaggedLogging.new(logger)
89 | end
90 |
91 | # Do not dump schema after migrations.
92 | config.active_record.dump_schema_after_migration = false
93 | end
94 |
--------------------------------------------------------------------------------
/graph-sample/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | Ruby Graph Sample
8 |
9 | <%= csrf_meta_tags %>
10 | <%= csp_meta_tag %>
11 | <%= favicon_link_tag "favicon.png" %>
12 |
13 |
14 |
18 |
20 |
21 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
22 | <%= javascript_importmap_tags %>
23 |
24 |
25 |
26 |
27 |
78 |
79 |
80 | <% if @errors %>
81 | <% @errors.each do |error| %>
82 |