├── .env ├── .gitignore ├── .rubocop-disabled.yml ├── .rubocop-enforced.yml ├── .rubocop.yml ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ └── welcome.js │ └── stylesheets │ │ ├── application.scss │ │ ├── buttons.scss │ │ └── nav_menu.scss ├── controllers │ ├── application_controller.rb │ ├── matter_controller.rb │ ├── profile_controller.rb │ └── welcome_controller.rb ├── helpers │ ├── identity_authentication_helper.rb │ └── manage_authorization_helper.rb ├── models │ └── application_record.rb └── views │ ├── application │ └── error.html.erb │ ├── layouts │ └── application.html.erb │ ├── matter │ ├── index.html.erb │ └── show.html.erb │ ├── profile │ └── index.html.erb │ └── welcome │ └── index.html.erb ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── environment.rb ├── environments │ ├── development.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── permissions_policy.rb │ ├── secure_headers.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── spring.rb └── storage.yml ├── docker-compose.yml ├── docs ├── docker_setup.md └── manual_setup.md ├── overlord.yml ├── public ├── 404.html ├── 422.html ├── 500.html ├── clio-logo-black.svg ├── clio-logo-blue.svg ├── clio-logo-white.svg ├── favicon.ico ├── robots.txt └── switchboard_logo.svg ├── scripts ├── entrypoint.sh └── resolve_docker_host.rb └── test ├── controllers └── welcome_controller_test.rb └── test_helper.rb /.env: -------------------------------------------------------------------------------- 1 | # Make sure you keep your secrets safe! How you do this is up to you, but in general: 2 | # * Never commit unencrypted secrets 3 | # * Don't share secrets in unencrypted messaging systems 4 | # * Store secrets safely 5 | 6 | CLIO_MANAGE_CLIENT_ID=YOUR_MANAGE_APP_CLIENT_ID 7 | CLIO_MANAGE_CLIENT_SECRET=YOUR_MANAGE_APP_CLIENT_SECRET 8 | CLIO_MANAGE_SITE_URL=https://app.clio.com/ 9 | 10 | CLIO_IDENTITY_CLIENT_ID=YOUR_IDENTITY_APP_CLIENT_ID 11 | CLIO_IDENTITY_CLIENT_SECRET=YOUR_IDENTITY_APP_CLIENT_SECRET 12 | CLIO_IDENTITY_SITE_URL=https://account.clio.com/ 13 | 14 | ROOT_URL=http://localhost:3013/ 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | .rspec 4 | /db/*.sqlite3 5 | /db/*.sqlite3-journal 6 | /db/*.sqlite3-[0-9]* 7 | /public/system 8 | /coverage/ 9 | /spec/tmp 10 | *.orig 11 | rerun.txt 12 | pickle-email-*.html 13 | /.idea/* 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | # Comment out this rule if you are OK with secrets being uploaded to the repo 22 | config/initializers/secret_token.rb 23 | config/master.key 24 | 25 | # Only include if you have production secrets in this file, which is no longer a Rails default 26 | # config/secrets.yml 27 | 28 | # dotenv, dotenv-rails 29 | # Comment out these rules if environment variables can be committed 30 | .env 31 | .env.* 32 | 33 | ## Environment normalization: 34 | 35 | # these should all be checked in to normalize the environment: 36 | # Gemfile.lock, .ruby-version, .ruby-gemset 37 | 38 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 39 | .rvmrc 40 | 41 | # if using bower-rails ignore default bower_components path bower.json files 42 | /vendor/assets/bower_components 43 | *.bowerrc 44 | bower.json 45 | 46 | # Ignore pow environment settings 47 | .powenv 48 | 49 | # Ignore Byebug command history file. 50 | .byebug_history 51 | 52 | # Ignore node_modules 53 | node_modules/ 54 | 55 | # Ignore precompiled javascript packs 56 | /public/packs 57 | /public/packs-test 58 | /public/assets 59 | 60 | # Ignore yarn files 61 | /yarn-error.log 62 | yarn-debug.log* 63 | .yarn-integrity 64 | 65 | # Ignore uploaded files in development 66 | /storage/* 67 | !/storage/.keep 68 | /public/uploads 69 | 70 | .DS_Store 71 | -------------------------------------------------------------------------------- /.rubocop-disabled.yml: -------------------------------------------------------------------------------- 1 | 2 | # These are all the cops that are disabled in the default configuration. 3 | 4 | # By default, the rails cops are not run. Override in project or home 5 | # directory .rubocop.yml files, or by giving the -R/--rails option. 6 | 7 | ################# Layout ################# 8 | 9 | Layout/BlockAlignment: 10 | Description: 'Align block ends correctly.' 11 | Enabled: false 12 | 13 | Layout/DefEndAlignment: 14 | Description: 'Align ends corresponding to defs correctly.' 15 | Enabled: false 16 | 17 | Layout/ElseAlignment: 18 | Description: 'Align elses and elsifs correctly.' 19 | Enabled: false 20 | 21 | Layout/EmptyLinesAroundAccessModifier: 22 | Description: "Keep blank lines around access modifiers." 23 | Enabled: false 24 | 25 | Layout/EmptyLinesAroundBlockBody: 26 | Description: "Keeps track of empty lines around block bodies." 27 | Enabled: false 28 | 29 | Layout/EmptyLinesAroundClassBody: 30 | Description: "Keeps track of empty lines around class bodies." 31 | Enabled: false 32 | 33 | Layout/EmptyLinesAroundModuleBody: 34 | Description: "Keeps track of empty lines around module bodies." 35 | Enabled: false 36 | 37 | Layout/EmptyLinesAroundMethodBody: 38 | Description: "Keeps track of empty lines around method bodies." 39 | Enabled: false 40 | 41 | Layout/EndAlignment: 42 | Description: 'Align ends correctly.' 43 | Enabled: false 44 | 45 | Layout/InitialIndentation: 46 | Description: >- 47 | Checks the indentation of the first non-blank non-comment line in a file. 48 | Enabled: false 49 | 50 | Layout/FirstArgumentIndentation: 51 | Description: 'Checks the indentation of the first parameter in a method call.' 52 | Enabled: false 53 | 54 | 55 | Layout/IndentationConsistency: 56 | Description: 'Keep indentation straight.' 57 | Enabled: false 58 | 59 | 60 | Layout/FirstArrayElementIndentation: 61 | Description: >- 62 | Checks the indentation of the first element in an array 63 | literal. 64 | Enabled: false 65 | 66 | Layout/AssignmentIndentation: 67 | Description: >- 68 | Checks the indentation of the first line of the 69 | right-hand-side of a multi-line assignment. 70 | Enabled: false 71 | 72 | Layout/FirstHashElementIndentation: 73 | Description: 'Checks the indentation of the first key in a hash literal.' 74 | Enabled: false 75 | 76 | Layout/IndentationStyle: 77 | Description: 'No hard tabs.' 78 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 79 | Enabled: false 80 | 81 | Layout/MultilineAssignmentLayout: 82 | Description: 'Check for a newline after the assignment operator in multi-line assignments.' 83 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-conditional-assignment' 84 | Enabled: false 85 | 86 | Layout/MultilineArrayBraceLayout: 87 | Description: >- 88 | Checks that the closing brace in an array literal is 89 | either on the same line as the last array element, or 90 | a new line. 91 | Enabled: false 92 | 93 | Layout/MultilineBlockLayout: 94 | Description: 'Ensures newlines after multiline block do statements.' 95 | Enabled: false 96 | 97 | Layout/MultilineHashBraceLayout: 98 | Description: >- 99 | Checks that the closing brace in a hash literal is 100 | either on the same line as the last hash element, or 101 | a new line. 102 | Enabled: false 103 | 104 | Layout/MultilineMethodCallBraceLayout: 105 | Description: >- 106 | Checks that the closing brace in a method call is 107 | either on the same line as the last method argument, or 108 | a new line. 109 | Enabled: false 110 | 111 | Layout/MultilineMethodCallIndentation: 112 | Description: >- 113 | Checks indentation of method calls with the dot operator 114 | that span more than one line. 115 | Enabled: false 116 | 117 | Layout/MultilineMethodDefinitionBraceLayout: 118 | Description: >- 119 | Checks that the closing brace in a method definition is 120 | either on the same line as the last method parameter, or 121 | a new line. 122 | Enabled: false 123 | 124 | Layout/MultilineOperationIndentation: 125 | Description: >- 126 | Checks indentation of binary operations that span more than 127 | one line. 128 | Enabled: false 129 | 130 | 131 | Layout/RescueEnsureAlignment: 132 | Description: 'Align rescues and ensures correctly.' 133 | Enabled: false 134 | 135 | Layout/SpaceBeforeFirstArg: 136 | Description: >- 137 | Checks that exactly one space is used between a method name 138 | and the first argument for method calls without parentheses. 139 | Enabled: false 140 | 141 | Layout/SpaceAfterColon: 142 | Description: 'Use spaces after colons.' 143 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 144 | Enabled: false 145 | 146 | Layout/SpaceAfterComma: 147 | Description: 'Use spaces after commas.' 148 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 149 | Enabled: false 150 | 151 | Layout/SpaceAfterSemicolon: 152 | Description: 'Use spaces after semicolons.' 153 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 154 | Enabled: false 155 | 156 | Layout/SpaceBeforeComment: 157 | Description: >- 158 | Checks for missing space between code and a comment on the 159 | same line. 160 | Enabled: false 161 | 162 | Layout/SpaceBeforeSemicolon: 163 | Description: 'No spaces before semicolons.' 164 | Enabled: false 165 | 166 | Layout/SpaceInsideBlockBraces: 167 | Description: >- 168 | Checks that block braces have or don't have surrounding space. 169 | For blocks taking parameters, checks that the left brace has 170 | or doesn't have trailing space. 171 | Enabled: false 172 | 173 | Layout/SpaceAroundBlockParameters: 174 | Description: 'Checks the spacing inside and after block parameters pipes.' 175 | Enabled: false 176 | 177 | Layout/SpaceInsideArrayPercentLiteral: 178 | Description: 'No unnecessary additional spaces between elements in %i/%w literals.' 179 | Enabled: false 180 | 181 | Layout/SpaceInsidePercentLiteralDelimiters: 182 | Description: 'No unnecessary spaces inside delimiters of %i/%w/%x literals.' 183 | Enabled: false 184 | 185 | ################# Style ####################### 186 | 187 | Style/AsciiComments: 188 | Description: 'Use only ascii symbols in comments.' 189 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' 190 | Enabled: false 191 | 192 | Style/AutoResourceCleanup: 193 | Description: 'Suggests the usage of an auto resource cleanup version of a method (if available).' 194 | Enabled: false 195 | 196 | Style/CollectionMethods: 197 | Description: 'Preferred collection methods.' 198 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size' 199 | Enabled: false 200 | 201 | Style/CommentAnnotation: 202 | Description: >- 203 | Checks formatting of special comments 204 | (TODO, FIXME, OPTIMIZE, HACK, REVIEW). 205 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' 206 | Enabled: false 207 | 208 | Style/Copyright: 209 | Description: 'Include a copyright notice in each file before any code.' 210 | Enabled: false 211 | 212 | Style/Encoding: 213 | Description: 'Use UTF-8 as the source file encoding.' 214 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#utf-8' 215 | Enabled: false 216 | 217 | Style/IfUnlessModifier: 218 | Description: >- 219 | Favor modifier if/unless usage when you have a 220 | single-line body. 221 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' 222 | Enabled: false 223 | 224 | Style/ImplicitRuntimeError: 225 | Description: >- 226 | Use `raise` or `fail` with an explicit exception class and 227 | message, rather than just a message. 228 | Enabled: false 229 | 230 | Style/InlineComment: 231 | Description: 'Avoid inline comments.' 232 | Enabled: false 233 | 234 | Style/MethodCalledOnDoEndBlock: 235 | Description: 'Avoid chaining a method call on a do...end block.' 236 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 237 | Enabled: false 238 | 239 | Style/MissingElse: 240 | Description: >- 241 | Require if/case expressions to have an else branches. 242 | If enabled, it is recommended that 243 | Style/UnlessElse and Style/EmptyElse be enabled. 244 | This will conflict with Style/EmptyElse if 245 | Style/EmptyElse is configured to style "both" 246 | Enabled: false 247 | EnforcedStyle: both 248 | SupportedStyles: 249 | # if - warn when an if expression is missing an else branch 250 | # case - warn when a case expression is missing an else branch 251 | # both - warn when an if or case expression is missing an else branch 252 | - if 253 | - case 254 | - both 255 | 256 | Style/OptionHash: 257 | Description: "Don't use option hashes when you can use keyword arguments." 258 | Enabled: false 259 | 260 | Style/StringMethods: 261 | Description: 'Checks if configured preferred methods are used over non-preferred.' 262 | Enabled: false 263 | 264 | Style/ClassAndModuleChildren: 265 | Description: 'Checks style of children classes and modules.' 266 | Enabled: false 267 | 268 | Style/ClassCheck: 269 | Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.' 270 | Enabled: false 271 | 272 | Style/ConditionalAssignment: 273 | Description: >- 274 | Use the return value of `if` and `case` statements for 275 | assignment to a variable and variable comparison instead 276 | of assigning that variable inside of each branch. 277 | Enabled: false 278 | 279 | Style/Documentation: 280 | Description: 'Document classes and non-namespace modules.' 281 | Enabled: false 282 | Exclude: 283 | - 'spec/**/*' 284 | - 'test/**/*' 285 | 286 | Style/DoubleNegation: 287 | Description: 'Checks for uses of double negation (!!).' 288 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' 289 | Enabled: false 290 | 291 | Style/EachForSimpleLoop: 292 | Description: >- 293 | Use `Integer#times` for a simple loop which iterates a fixed 294 | number of times. 295 | Enabled: false 296 | 297 | Style/EachWithObject: 298 | Description: 'Prefer `each_with_object` over `inject` or `reduce`.' 299 | Enabled: false 300 | 301 | Style/EmptyElse: 302 | Description: 'Avoid empty else-clauses.' 303 | Enabled: false 304 | 305 | Style/EmptyCaseCondition: 306 | Description: 'Avoid empty condition in case statements.' 307 | Enabled: false 308 | 309 | Style/EndBlock: 310 | Description: 'END blocks should not be placed inside method definitions.' 311 | Enabled: false 312 | 313 | Style/FormatString: 314 | Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' 315 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' 316 | Enabled: false 317 | 318 | Style/GuardClause: 319 | Description: 'Check for conditionals that can be replaced with guard clauses' 320 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 321 | Enabled: false 322 | 323 | Style/IfInsideElse: 324 | Description: 'Finds if nodes inside else, which can be converted to elsif.' 325 | Enabled: false 326 | 327 | Style/IdenticalConditionalBranches: 328 | Description: >- 329 | Checks that conditional statements do not have an identical 330 | line at the end of each branch, which can validly be moved 331 | out of the conditional. 332 | Enabled: false 333 | 334 | Style/LineEndConcatenation: 335 | Description: >- 336 | Use \ instead of + or << to concatenate two string literals at 337 | line end. 338 | Enabled: false 339 | 340 | Style/MethodDefParentheses: 341 | Description: >- 342 | Checks if the method definitions have or don't have 343 | parentheses. 344 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 345 | Enabled: false 346 | 347 | Style/MultilineBlockChain: 348 | Description: 'Avoid multi-line chains of blocks.' 349 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 350 | Enabled: false 351 | 352 | Style/MutableConstant: 353 | Description: 'Do not assign mutable objects to constants.' 354 | Enabled: false 355 | 356 | Style/NilComparison: 357 | Description: 'Prefer x.nil? to x == nil.' 358 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 359 | Enabled: false 360 | 361 | Style/NegatedIf: 362 | Description: >- 363 | Favor unless over if for negative conditions 364 | (or control flow or). 365 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' 366 | Enabled: false 367 | 368 | Style/NestedParenthesizedCalls: 369 | Description: >- 370 | Parenthesize method calls which are nested inside the 371 | argument list of another parenthesized method call. 372 | Enabled: false 373 | 374 | Style/NumericPredicate: 375 | Description: >- 376 | Checks for the use of predicate- or comparison methods for 377 | numeric comparisons. 378 | Enabled: false 379 | 380 | Style/OneLineConditional: 381 | Description: >- 382 | Favor the ternary operator(?:) over 383 | if/then/else/end constructs. 384 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' 385 | Enabled: false 386 | 387 | Style/PercentQLiterals: 388 | Description: 'Checks if uses of %Q/%q match the configured preference.' 389 | Enabled: false 390 | 391 | Style/PerlBackrefs: 392 | Description: 'Avoid Perl-style regex back references.' 393 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' 394 | Enabled: false 395 | 396 | Style/Proc: 397 | Description: 'Use proc instead of Proc.new.' 398 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' 399 | Enabled: false 400 | 401 | Style/RedundantFreeze: 402 | Description: "Checks usages of Object#freeze on immutable objects." 403 | Enabled: false 404 | 405 | Style/RegexpLiteral: 406 | Description: 'Use / or %r around regular expressions.' 407 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' 408 | Enabled: false 409 | 410 | Style/RedundantParentheses: 411 | Description: "Checks for parentheses that seem not to serve any purpose." 412 | Enabled: false 413 | 414 | Style/SingleLineBlockParams: 415 | Description: 'Enforces the names of some block params.' 416 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' 417 | Enabled: false 418 | 419 | Style/SingleLineMethods: 420 | Description: 'Avoid single-line methods.' 421 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' 422 | Enabled: false 423 | 424 | Style/StringLiterals: 425 | Description: 'Checks if uses of quotes match the configured preference.' 426 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals' 427 | Enabled: false 428 | 429 | Style/StringLiteralsInInterpolation: 430 | Description: >- 431 | Checks if uses of quotes inside expressions in interpolated 432 | strings match the configured preference. 433 | Enabled: false 434 | 435 | Style/SymbolArray: 436 | Description: 'Use %i or %I for arrays of symbols.' 437 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-i' 438 | Enabled: false 439 | 440 | Style/SymbolLiteral: 441 | Description: 'Use plain symbols instead of string symbols when possible.' 442 | Enabled: false 443 | 444 | Style/SymbolProc: 445 | Description: 'Use symbols as procs instead of blocks when possible.' 446 | Enabled: false 447 | 448 | Style/TernaryParentheses: 449 | Description: 'Checks for use of parentheses around ternary conditions.' 450 | Enabled: false 451 | 452 | Style/TrailingCommaInArguments: 453 | Description: 'Checks for trailing comma in argument lists.' 454 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma' 455 | Enabled: false 456 | 457 | Style/TrailingCommaInArrayLiteral: 458 | Description: 'Checks for trailing comma in array literals.' 459 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 460 | Enabled: false 461 | 462 | Style/TrailingCommaInHashLiteral: 463 | Description: 'Checks for trailing comma in hash literals.' 464 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 465 | Enabled: false 466 | 467 | Style/RedundantCapitalW: 468 | Description: 'Checks for %W when interpolation is not needed.' 469 | Enabled: false 470 | 471 | Style/RedundantInterpolation: 472 | Description: 'Checks for strings that are just an interpolated expression.' 473 | Enabled: false 474 | 475 | Style/TrailingUnderscoreVariable: 476 | Description: >- 477 | Checks for the usage of unneeded trailing underscores at the 478 | end of parallel variable assignment. 479 | AllowNamedUnderscoreVariables: true 480 | Enabled: false 481 | 482 | Style/WordArray: 483 | Description: 'Use %w or %W for arrays of words.' 484 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' 485 | Enabled: false 486 | 487 | Style/ZeroLengthPredicate: 488 | Description: 'Use #empty? when testing for objects of length 0.' 489 | Enabled: false 490 | 491 | Style/RedundantSortBy: 492 | Description: 'Use `sort` instead of `sort_by { |x| x }`.' 493 | Enabled: false 494 | 495 | Style/Sample: 496 | Description: >- 497 | Use `sample` instead of `shuffle.first`, 498 | `shuffle.last`, and `shuffle[Fixnum]`. 499 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' 500 | Enabled: false 501 | 502 | ################### Metrics ################################ 503 | 504 | Metrics/AbcSize: 505 | Description: >- 506 | A calculated magnitude based on number of assignments, 507 | branches, and conditions. 508 | Reference: 'http://c2.com/cgi/wiki?AbcMetric' 509 | Enabled: false 510 | 511 | Metrics/ClassLength: 512 | Description: 'Avoid classes longer than 100 lines of code.' 513 | Enabled: false 514 | 515 | Metrics/ModuleLength: 516 | Description: 'Avoid modules longer than 100 lines of code.' 517 | Enabled: false 518 | 519 | Metrics/CyclomaticComplexity: 520 | Description: >- 521 | A complexity metric that is strongly correlated to the number 522 | of test cases needed to validate a method. 523 | Enabled: false 524 | 525 | Metrics/PerceivedComplexity: 526 | Description: >- 527 | A complexity metric geared towards measuring complexity for a 528 | human reader. 529 | Enabled: false 530 | 531 | ################### Lint ################################ 532 | ## Warnings 533 | Lint/AmbiguousRegexpLiteral: 534 | Description: >- 535 | Checks for ambiguous regexp literals in the first argument of 536 | a method invocation without parentheses. 537 | Enabled: false 538 | 539 | Lint/AssignmentInCondition: 540 | Description: "Don't use assignment in conditions." 541 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition' 542 | Enabled: false 543 | 544 | Lint/DeprecatedClassMethods: 545 | Description: 'Check for deprecated class method calls.' 546 | Enabled: false 547 | 548 | Lint/DuplicateMethods: 549 | Description: 'Check for duplicate method definitions.' 550 | Enabled: false 551 | 552 | Lint/EachWithObjectArgument: 553 | Description: 'Check for immutable argument given to each_with_object.' 554 | Enabled: false 555 | 556 | Lint/ElseLayout: 557 | Description: 'Check for odd code arrangement in an else block.' 558 | Enabled: false 559 | 560 | Lint/EmptyEnsure: 561 | Description: 'Checks for empty ensure block.' 562 | Enabled: false 563 | 564 | Lint/EmptyInterpolation: 565 | Description: 'Checks for empty string interpolation.' 566 | Enabled: false 567 | 568 | Lint/FlipFlop: 569 | Description: 'Checks for flip flops' 570 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' 571 | Enabled: false 572 | 573 | Lint/FloatOutOfRange: 574 | Description: >- 575 | Catches floating-point literals too large or small for Ruby to 576 | represent. 577 | Enabled: false 578 | 579 | Lint/FormatParameterMismatch: 580 | Description: 'The number of parameters to format/sprint must match the fields.' 581 | Enabled: false 582 | 583 | Lint/ImplicitStringConcatenation: 584 | Description: >- 585 | Checks for adjacent string literals on the same line, which 586 | could better be represented as a single string literal. 587 | Enabled: false 588 | 589 | Lint/IneffectiveAccessModifier: 590 | Description: >- 591 | Checks for attempts to use `private` or `protected` to set 592 | the visibility of a class method, which does not work. 593 | Enabled: false 594 | 595 | Lint/InheritException: 596 | Description: 'Avoid inheriting from the `Exception` class.' 597 | Enabled: false 598 | 599 | Lint/LiteralAsCondition: 600 | Description: 'Checks of literals used in conditions.' 601 | Enabled: false 602 | 603 | Lint/LiteralInInterpolation: 604 | Description: 'Checks for literals used in interpolation.' 605 | Enabled: false 606 | 607 | Lint/MissingSuper: 608 | Enabled: false 609 | 610 | Lint/NextWithoutAccumulator: 611 | Description: >- 612 | Do not omit the accumulator when calling `next` 613 | in a `reduce`/`inject` block. 614 | Enabled: false 615 | 616 | Lint/NonLocalExitFromIterator: 617 | Description: 'Do not use return in iterator to cause non-local exit.' 618 | Enabled: false 619 | 620 | Lint/ParenthesesAsGroupedExpression: 621 | Description: >- 622 | Checks for method calls with a space before the opening 623 | parenthesis. 624 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 625 | Enabled: false 626 | 627 | Lint/PercentStringArray: 628 | Description: >- 629 | Checks for unwanted commas and quotes in %w/%W literals. 630 | Enabled: false 631 | 632 | Lint/PercentSymbolArray: 633 | Description: >- 634 | Checks for unwanted commas and colons in %i/%I literals. 635 | Enabled: false 636 | 637 | Lint/RandOne: 638 | Description: >- 639 | Checks for `rand(1)` calls. Such calls always return `0` 640 | and most likely a mistake. 641 | Enabled: false 642 | 643 | Lint/RequireParentheses: 644 | Description: >- 645 | Use parentheses in the method call to avoid confusion 646 | about precedence. 647 | Enabled: false 648 | 649 | Lint/ShadowedException: 650 | Description: >- 651 | Avoid rescuing a higher level exception 652 | before a lower level exception. 653 | Enabled: false 654 | 655 | Lint/ShadowingOuterLocalVariable: 656 | Description: >- 657 | Do not use the same name as outer local variable 658 | for block arguments or block local variables. 659 | Enabled: false 660 | 661 | Lint/RedundantCopDisableDirective: 662 | Description: >- 663 | Checks for rubocop:disable comments that can be removed. 664 | Note: this cop is not disabled when disabling all cops. 665 | It must be explicitly disabled. 666 | Enabled: false 667 | 668 | Lint/UnderscorePrefixedVariableName: 669 | Description: 'Do not use prefix `_` for a variable that is used.' 670 | Enabled: false 671 | 672 | Lint/UselessAccessModifier: 673 | Description: 'Checks for useless access modifiers.' 674 | Enabled: false 675 | ContextCreatingMethods: [] 676 | 677 | Lint/UselessElseWithoutRescue: 678 | Description: 'Checks for useless `else` in `begin..end` without `rescue`.' 679 | Enabled: false 680 | 681 | Lint/UselessSetterCall: 682 | Description: 'Checks for useless setter call to a local variable.' 683 | Enabled: false 684 | 685 | Lint/Void: 686 | Description: 'Possible use of operator/literal/variable in void context.' 687 | Enabled: false 688 | 689 | #################### Rails ################################## 690 | 691 | Rails/ActionFilter: 692 | Description: 'Enforces consistent use of action filter methods.' 693 | Enabled: false 694 | 695 | Rails/Date: 696 | Description: >- 697 | Checks the correct usage of date aware methods, 698 | such as Date.today, Date.current etc. 699 | Enabled: false 700 | 701 | Rails/Delegate: 702 | Description: 'Prefer delegate method for delegations.' 703 | Enabled: false 704 | 705 | Rails/Exit: 706 | Description: >- 707 | Favor `fail`, `break`, `return`, etc. over `exit` in 708 | application or library code outside of Rake files to avoid 709 | exits during unit testing or running in production. 710 | Enabled: false 711 | 712 | Rails/FindBy: 713 | Description: 'Prefer find_by over where.first.' 714 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#find_by' 715 | Enabled: false 716 | 717 | Rails/FindEach: 718 | Description: 'Prefer all.find_each over all.find.' 719 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#find-each' 720 | Enabled: false 721 | 722 | Rails/HasAndBelongsToMany: 723 | Description: 'Prefer has_many :through to has_and_belongs_to_many.' 724 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through' 725 | Enabled: false 726 | 727 | Rails/Output: 728 | Description: 'Checks for calls to puts, print, etc.' 729 | Enabled: false 730 | 731 | Rails/PluralizationGrammar: 732 | Description: 'Checks for incorrect grammar when using methods like `3.day.ago`.' 733 | Enabled: false 734 | 735 | Rails/ReadWriteAttribute: 736 | Description: >- 737 | Checks for read_attribute(:attr) and 738 | write_attribute(:attr, val). 739 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#read-attribute' 740 | Enabled: false 741 | 742 | Rails/RequestReferer: 743 | Description: 'Use consistent syntax for request.referer.' 744 | Enabled: false 745 | 746 | Rails/ScopeArgs: 747 | Description: 'Checks the arguments of ActiveRecord scopes.' 748 | Enabled: false 749 | 750 | Rails/TimeZone: 751 | Description: 'Checks the correct usage of time zone aware methods.' 752 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' 753 | Reference: 'http://danilenko.org/2012/7/6/rails_timezones' 754 | Enabled: false 755 | 756 | Rails/UniqBeforePluck: 757 | Description: 'Prefer the use of uniq or distinct before pluck.' 758 | Enabled: false 759 | 760 | Rails/Validation: 761 | Description: 'Use validates :attribute, hash of validations.' 762 | Enabled: false 763 | 764 | ################# Naming #################### 765 | 766 | Naming/VariableName: 767 | Description: 'Use the configured style when naming variables.' 768 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 769 | Enabled: false 770 | 771 | -------------------------------------------------------------------------------- /.rubocop-enforced.yml: -------------------------------------------------------------------------------- 1 | # Things added to this file are enforced by CI 2 | require: 3 | - rubocop-rails 4 | AllCops: 5 | DisabledByDefault: true 6 | Include: 7 | - '**/*.rb' 8 | - '**/*.rake' 9 | - '**/config.ru' 10 | - '**/Gemfile' 11 | - '**/Rakefile' 12 | 13 | ################# Layout ################# 14 | Layout/DefEndAlignment: 15 | Description: 'Align ends corresponding to defs correctly.' 16 | EnforcedStyleAlignWith: start_of_line 17 | Enabled: true 18 | 19 | Layout/EmptyLines: 20 | Description: "This cop checks for two or more consecutive blank lines." 21 | StyleGuide: "https://github.com/rubocop-hq/ruby-style-guide#two-or-more-empty-lines" 22 | Enabled: true 23 | 24 | Layout/EmptyLineBetweenDefs: 25 | Description: 'Use empty lines between defs.' 26 | # EmptyLineBetweenClassDefs disabled for now to preserve old behaviour 27 | # added in 1.4.0 https://github.com/rubocop/rubocop/releases/tag/v1.4.0 28 | EmptyLineBetweenClassDefs: false 29 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods' 30 | Enabled: true 31 | 32 | Layout/EndOfLine: 33 | Description: 'Use Unix-style line endings.' 34 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' 35 | Enabled: true 36 | 37 | Layout/ExtraSpacing: 38 | AllowForAlignment: true 39 | Description: 'Do not use unnecessary spacing.' 40 | Enabled: true 41 | 42 | Layout/HeredocIndentation: 43 | Description: >- 44 | This cops checks the indentation of the here document bodies. 45 | The bodies are indented one step. In Ruby 2.3 or newer, squiggly heredocs (<<~) should be used. 46 | Enabled: true 47 | 48 | Layout/SpaceAfterMethodName: 49 | Description: >- 50 | Do not put a space between a method name and the opening 51 | parenthesis in a method definition. 52 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 53 | Enabled: true 54 | 55 | Layout/SpaceAroundKeyword: 56 | Description: 'Use a space around keywords if appropriate.' 57 | Enabled: true 58 | 59 | Layout/SpaceAroundOperators: 60 | Description: 'Use a single space around operators.' 61 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 62 | Enabled: true 63 | 64 | Layout/SpaceBeforeBlockBraces: 65 | Description: >- 66 | Checks that the left block brace has or doesn't have space 67 | before it. 68 | Enabled: true 69 | Exclude: 70 | - "**/spec/**/*.rb" 71 | 72 | Layout/SpaceBeforeComma: 73 | Description: 'No spaces before commas.' 74 | Enabled: true 75 | 76 | Layout/SpaceInsideHashLiteralBraces: 77 | Description: "Use spaces inside hash literal braces - or don't." 78 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 79 | Enabled: true 80 | 81 | Layout/TrailingEmptyLines: 82 | # See also: https://robots.thoughtbot.com/no-newline-at-end-of-file 83 | Description: 'Checks trailing blank lines and final newline.' 84 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' 85 | Enabled: true 86 | 87 | Layout/TrailingWhitespace: 88 | Enabled: true 89 | 90 | ################# Lint ####################### 91 | Lint/CircularArgumentReference: 92 | Description: "Default values in optional keyword arguments and optional ordinal arguments should not refer back to the name of the argument." 93 | Enabled: true 94 | 95 | Lint/Debugger: 96 | Description: 'Check for debugger calls.' 97 | Enabled: true 98 | 99 | Lint/DeprecatedClassMethods: 100 | Enabled: true 101 | 102 | Lint/DuplicateHashKey: 103 | Description: 'Check for duplicate keys in hash literals.' 104 | Enabled: true 105 | 106 | Lint/EnsureReturn: 107 | Enabled: true 108 | 109 | Lint/InheritException: 110 | Enabled: true 111 | 112 | Lint/NestedMethodDefinition: 113 | Description: 'Do not use nested method definitions.' 114 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods' 115 | Enabled: true 116 | 117 | Lint/RescueException: 118 | Enabled: true 119 | 120 | Lint/RedundantStringCoercion: 121 | Enabled: true 122 | 123 | Lint/UnreachableCode: 124 | Enabled: true 125 | 126 | Lint/UnusedBlockArgument: 127 | Description: 'Checks for unused block arguments.' 128 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 129 | Enabled: true 130 | 131 | ################# Naming ####################### 132 | Naming/AsciiIdentifiers: 133 | Description: 'Use only ascii symbols in identifiers.' 134 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' 135 | Enabled: true 136 | 137 | Naming/ClassAndModuleCamelCase: 138 | Description: 'Use CamelCase for classes and modules.' 139 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes' 140 | Enabled: true 141 | 142 | Naming/ConstantName: 143 | Description: 'Constants should use SCREAMING_SNAKE_CASE.' 144 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case' 145 | Enabled: true 146 | 147 | Naming/FileName: 148 | Description: 'Use snake_case for source file names.' 149 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' 150 | Enabled: true 151 | 152 | ################# Style ####################### 153 | Style/ArrayJoin: 154 | Description: 'Use Array#join instead of Array#*.' 155 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' 156 | Enabled: true 157 | 158 | Style/BeginBlock: 159 | Description: 'Avoid the use of BEGIN blocks.' 160 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks' 161 | Enabled: true 162 | 163 | Style/BlockComments: 164 | Description: 'Do not use block comments.' 165 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments' 166 | Enabled: true 167 | 168 | Style/BlockDelimiters: 169 | Description: >- 170 | Avoid using {...} for multi-line blocks (multiline chaining is 171 | always ugly). 172 | Prefer {...} over do...end for single-line blocks. 173 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 174 | Enabled: true 175 | 176 | Style/EndBlock: 177 | Description: 'Avoid the use of END blocks.' 178 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' 179 | Enabled: true 180 | 181 | Style/HashSyntax: 182 | Enabled: true 183 | EnforcedStyle: ruby19_no_mixed_keys 184 | Exclude: 185 | 186 | Style/IfUnlessModifierOfIfUnless: 187 | Description: >- 188 | Avoid modifier if/unless usage on conditionals. 189 | Enabled: true 190 | 191 | Style/IfWithSemicolon: 192 | Description: 'Do not use if x; .... Use the ternary operator instead.' 193 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' 194 | Enabled: true 195 | 196 | Style/MethodCallWithoutArgsParentheses: 197 | Description: 'Do not use parentheses for method calls with no arguments.' 198 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' 199 | Enabled: true 200 | 201 | Style/MissingRespondToMissing: 202 | Enabled: true 203 | 204 | Style/RaiseArgs: 205 | Description: 'Checks the arguments passed to raise/fail.' 206 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages' 207 | EnforcedStyle: compact 208 | Enabled: false 209 | 210 | Style/RedundantReturn: 211 | Description: "Don't use return where it's not required." 212 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' 213 | Enabled: true 214 | 215 | Style/Semicolon: 216 | Description: "Don't use semicolons to terminate expressions." 217 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon' 218 | Enabled: true 219 | 220 | Style/SpecialGlobalVars: 221 | Description: 'Avoid Perl-style global variables.' 222 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' 223 | Enabled: true 224 | 225 | Style/StringLiterals: 226 | EnforcedStyle: double_quotes 227 | Description: 'Prefer double-quotes unless your string literal contains " or escape characters you want to suppress.' 228 | Enabled: true 229 | 230 | ################# Rails Cops ########################## 231 | 232 | Rails/DynamicFindBy: 233 | Description: "Favor the use of find_by over where.take and find_by_attribute when you need to retrieve a single record by one or more attributes and return nil when the record is not found" 234 | Enabled: true 235 | 236 | Rails/EnumHash: 237 | Description: "This cop looks for enums written with array syntax." 238 | Enabled: true 239 | 240 | Rails/HttpPositionalArguments: 241 | Enabled: true 242 | 243 | Rails/ActiveRecordAliases: 244 | Enabled: true 245 | 246 | ################# Security #################### 247 | 248 | Rails/OutputSafety: 249 | Description: 'The use of `html_safe` or `raw` may be a security risk.' 250 | Enabled: true 251 | Exclude: 252 | - "**/spec/**/*.rb" 253 | 254 | Security/Eval: 255 | Description: 'The use of eval represents a serious security risk.' 256 | Enabled: true 257 | 258 | Security/JSONLoad: 259 | Enabled: true 260 | Exclude: 261 | - "**/spec/**/*.rb" 262 | - "script/**/*.rb" 263 | 264 | Security/MarshalLoad: 265 | Enabled: true 266 | 267 | Security/Open: 268 | Enabled: true 269 | 270 | Security/YAMLLoad: 271 | Enabled: true 272 | Exclude: 273 | - "**/spec/**/*.rb" 274 | - "script/*" 275 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rails 2 | inherit_from: 3 | - .rubocop-enforced.yml 4 | - .rubocop-disabled.yml 5 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.6 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.1.6-bullseye 2 | 3 | RUN apt-get update -qq && apt-get install -y nodejs 4 | 5 | ENV APP_HOME /Switchboard 6 | WORKDIR $APP_HOME 7 | COPY . . 8 | 9 | RUN gem install bundler:2.2.21 && bundle install 10 | 11 | COPY ./scripts/entrypoint.sh /usr/bin/ 12 | RUN chmod +x /usr/bin/entrypoint.sh 13 | COPY ./scripts/resolve_docker_host.rb /usr/bin/ 14 | RUN chmod +x /usr/bin/resolve_docker_host.rb 15 | ENTRYPOINT ["entrypoint.sh"] 16 | 17 | EXPOSE 3013 18 | 19 | CMD ["rails", "server", "-p", "3013", "-b", "0.0.0.0"] 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | # Bundle edge Rails instead: gem "rails", github: "rails/rails" 9 | gem "rails", "7.0.8.4" 10 | # Use Puma as the app server 11 | gem "puma" 12 | # Use SCSS for stylesheets 13 | gem "sass-rails", "~> 5.0" 14 | # Use Uglifier as compressor for JavaScript assets 15 | gem "uglifier", ">= 1.3.0" 16 | # Use CoffeeScript for .coffee assets and views 17 | gem "coffee-rails", "~> 4.2" 18 | # Used to enforce code style 19 | gem "rubocop-rails", "2.15.0" 20 | # Use jquery as the JavaScript library 21 | gem "jquery-rails" 22 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 23 | gem "turbolinks", "~> 5" 24 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 25 | gem "jbuilder", "~> 2.5" 26 | 27 | gem "http", "~> 5" 28 | gem "jwt", "2.5.0" 29 | gem "secure_headers", "6.3.1" 30 | 31 | group :development, :test do 32 | # Call "byebug" anywhere in the code to stop execution and get a debugger console 33 | gem "byebug", platform: :mri 34 | gem "dotenv-rails" 35 | end 36 | 37 | group :development do 38 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 39 | gem "web-console", ">= 3.3.0" 40 | gem "listen" 41 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 42 | gem "spring" 43 | gem "spring-watcher-listen" 44 | gem "pry" 45 | gem "pry-byebug" 46 | end 47 | 48 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 49 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] 50 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (7.0.8.4) 5 | actionpack (= 7.0.8.4) 6 | activesupport (= 7.0.8.4) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (7.0.8.4) 10 | actionpack (= 7.0.8.4) 11 | activejob (= 7.0.8.4) 12 | activerecord (= 7.0.8.4) 13 | activestorage (= 7.0.8.4) 14 | activesupport (= 7.0.8.4) 15 | mail (>= 2.7.1) 16 | net-imap 17 | net-pop 18 | net-smtp 19 | actionmailer (7.0.8.4) 20 | actionpack (= 7.0.8.4) 21 | actionview (= 7.0.8.4) 22 | activejob (= 7.0.8.4) 23 | activesupport (= 7.0.8.4) 24 | mail (~> 2.5, >= 2.5.4) 25 | net-imap 26 | net-pop 27 | net-smtp 28 | rails-dom-testing (~> 2.0) 29 | actionpack (7.0.8.4) 30 | actionview (= 7.0.8.4) 31 | activesupport (= 7.0.8.4) 32 | rack (~> 2.0, >= 2.2.4) 33 | rack-test (>= 0.6.3) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 36 | actiontext (7.0.8.4) 37 | actionpack (= 7.0.8.4) 38 | activerecord (= 7.0.8.4) 39 | activestorage (= 7.0.8.4) 40 | activesupport (= 7.0.8.4) 41 | globalid (>= 0.6.0) 42 | nokogiri (>= 1.8.5) 43 | actionview (7.0.8.4) 44 | activesupport (= 7.0.8.4) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (7.0.8.4) 50 | activesupport (= 7.0.8.4) 51 | globalid (>= 0.3.6) 52 | activemodel (7.0.8.4) 53 | activesupport (= 7.0.8.4) 54 | activerecord (7.0.8.4) 55 | activemodel (= 7.0.8.4) 56 | activesupport (= 7.0.8.4) 57 | activestorage (7.0.8.4) 58 | actionpack (= 7.0.8.4) 59 | activejob (= 7.0.8.4) 60 | activerecord (= 7.0.8.4) 61 | activesupport (= 7.0.8.4) 62 | marcel (~> 1.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (7.0.8.4) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | addressable (2.8.6) 70 | public_suffix (>= 2.0.2, < 6.0) 71 | ast (2.4.2) 72 | base64 (0.2.0) 73 | bindex (0.8.1) 74 | builder (3.3.0) 75 | byebug (11.1.3) 76 | coderay (1.1.3) 77 | coffee-rails (4.2.2) 78 | coffee-script (>= 2.2.0) 79 | railties (>= 4.0.0) 80 | coffee-script (2.4.1) 81 | coffee-script-source 82 | execjs 83 | coffee-script-source (1.12.2) 84 | concurrent-ruby (1.3.3) 85 | crass (1.0.6) 86 | date (3.3.4) 87 | domain_name (0.6.20240107) 88 | dotenv (2.8.1) 89 | dotenv-rails (2.8.1) 90 | dotenv (= 2.8.1) 91 | railties (>= 3.2) 92 | erubi (1.13.0) 93 | execjs (2.9.1) 94 | ffi (1.17.0) 95 | ffi-compiler (1.3.2) 96 | ffi (>= 1.15.5) 97 | rake 98 | globalid (1.2.1) 99 | activesupport (>= 6.1) 100 | http (5.2.0) 101 | addressable (~> 2.8) 102 | base64 (~> 0.1) 103 | http-cookie (~> 1.0) 104 | http-form_data (~> 2.2) 105 | llhttp-ffi (~> 0.5.0) 106 | http-cookie (1.0.6) 107 | domain_name (~> 0.5) 108 | http-form_data (2.3.0) 109 | i18n (1.14.5) 110 | concurrent-ruby (~> 1.0) 111 | jbuilder (2.12.0) 112 | actionview (>= 5.0.0) 113 | activesupport (>= 5.0.0) 114 | jquery-rails (4.6.0) 115 | rails-dom-testing (>= 1, < 3) 116 | railties (>= 4.2.0) 117 | thor (>= 0.14, < 2.0) 118 | json (2.7.2) 119 | jwt (2.5.0) 120 | listen (3.9.0) 121 | rb-fsevent (~> 0.10, >= 0.10.3) 122 | rb-inotify (~> 0.9, >= 0.9.10) 123 | llhttp-ffi (0.5.0) 124 | ffi-compiler (~> 1.0) 125 | rake (~> 13.0) 126 | loofah (2.22.0) 127 | crass (~> 1.0.2) 128 | nokogiri (>= 1.12.0) 129 | mail (2.8.1) 130 | mini_mime (>= 0.1.1) 131 | net-imap 132 | net-pop 133 | net-smtp 134 | marcel (1.0.4) 135 | method_source (1.1.0) 136 | mini_mime (1.1.5) 137 | mini_portile2 (2.8.7) 138 | minitest (5.23.1) 139 | net-imap (0.4.14) 140 | date 141 | net-protocol 142 | net-pop (0.1.2) 143 | net-protocol 144 | net-protocol (0.2.2) 145 | timeout 146 | net-smtp (0.5.0) 147 | net-protocol 148 | nio4r (2.7.3) 149 | nokogiri (1.13.10) 150 | mini_portile2 (~> 2.8.0) 151 | racc (~> 1.4) 152 | parallel (1.24.0) 153 | parser (3.3.3.0) 154 | ast (~> 2.4.1) 155 | racc 156 | pry (0.14.2) 157 | coderay (~> 1.1) 158 | method_source (~> 1.0) 159 | pry-byebug (3.8.0) 160 | byebug (~> 11.0) 161 | pry (~> 0.10) 162 | public_suffix (5.1.1) 163 | puma (6.4.2) 164 | nio4r (~> 2.0) 165 | racc (1.8.0) 166 | rack (2.2.9) 167 | rack-test (2.1.0) 168 | rack (>= 1.3) 169 | rails (7.0.8.4) 170 | actioncable (= 7.0.8.4) 171 | actionmailbox (= 7.0.8.4) 172 | actionmailer (= 7.0.8.4) 173 | actionpack (= 7.0.8.4) 174 | actiontext (= 7.0.8.4) 175 | actionview (= 7.0.8.4) 176 | activejob (= 7.0.8.4) 177 | activemodel (= 7.0.8.4) 178 | activerecord (= 7.0.8.4) 179 | activestorage (= 7.0.8.4) 180 | activesupport (= 7.0.8.4) 181 | bundler (>= 1.15.0) 182 | railties (= 7.0.8.4) 183 | rails-dom-testing (2.2.0) 184 | activesupport (>= 5.0.0) 185 | minitest 186 | nokogiri (>= 1.6) 187 | rails-html-sanitizer (1.5.0) 188 | loofah (~> 2.19, >= 2.19.1) 189 | railties (7.0.8.4) 190 | actionpack (= 7.0.8.4) 191 | activesupport (= 7.0.8.4) 192 | method_source 193 | rake (>= 12.2) 194 | thor (~> 1.0) 195 | zeitwerk (~> 2.5) 196 | rainbow (3.1.1) 197 | rake (13.2.1) 198 | rb-fsevent (0.11.2) 199 | rb-inotify (0.11.1) 200 | ffi (~> 1.0) 201 | regexp_parser (2.9.2) 202 | rexml (3.3.0) 203 | strscan 204 | rubocop (1.50.2) 205 | json (~> 2.3) 206 | parallel (~> 1.10) 207 | parser (>= 3.2.0.0) 208 | rainbow (>= 2.2.2, < 4.0) 209 | regexp_parser (>= 1.8, < 3.0) 210 | rexml (>= 3.2.5, < 4.0) 211 | rubocop-ast (>= 1.28.0, < 2.0) 212 | ruby-progressbar (~> 1.7) 213 | unicode-display_width (>= 2.4.0, < 3.0) 214 | rubocop-ast (1.30.0) 215 | parser (>= 3.2.1.0) 216 | rubocop-rails (2.15.0) 217 | activesupport (>= 4.2.0) 218 | rack (>= 1.1) 219 | rubocop (>= 1.7.0, < 2.0) 220 | ruby-progressbar (1.13.0) 221 | sass (3.7.4) 222 | sass-listen (~> 4.0.0) 223 | sass-listen (4.0.0) 224 | rb-fsevent (~> 0.9, >= 0.9.4) 225 | rb-inotify (~> 0.9, >= 0.9.7) 226 | sass-rails (5.1.0) 227 | railties (>= 5.2.0) 228 | sass (~> 3.1) 229 | sprockets (>= 2.8, < 4.0) 230 | sprockets-rails (>= 2.0, < 4.0) 231 | tilt (>= 1.1, < 3) 232 | secure_headers (6.3.1) 233 | spring (4.2.1) 234 | spring-watcher-listen (2.1.0) 235 | listen (>= 2.7, < 4.0) 236 | spring (>= 4) 237 | sprockets (3.7.3) 238 | base64 239 | concurrent-ruby (~> 1.0) 240 | rack (> 1, < 3) 241 | sprockets-rails (3.4.2) 242 | actionpack (>= 5.2) 243 | activesupport (>= 5.2) 244 | sprockets (>= 3.0.0) 245 | strscan (3.1.0) 246 | thor (1.3.1) 247 | tilt (2.3.0) 248 | timeout (0.4.1) 249 | turbolinks (5.2.1) 250 | turbolinks-source (~> 5.2) 251 | turbolinks-source (5.2.0) 252 | tzinfo (2.0.6) 253 | concurrent-ruby (~> 1.0) 254 | uglifier (4.2.0) 255 | execjs (>= 0.3.0, < 3) 256 | unicode-display_width (2.5.0) 257 | web-console (4.2.1) 258 | actionview (>= 6.0.0) 259 | activemodel (>= 6.0.0) 260 | bindex (>= 0.4.0) 261 | railties (>= 6.0.0) 262 | websocket-driver (0.7.6) 263 | websocket-extensions (>= 0.1.0) 264 | websocket-extensions (0.1.5) 265 | zeitwerk (2.6.15) 266 | 267 | PLATFORMS 268 | ruby 269 | 270 | DEPENDENCIES 271 | byebug 272 | coffee-rails (~> 4.2) 273 | dotenv-rails 274 | http (~> 5) 275 | jbuilder (~> 2.5) 276 | jquery-rails 277 | jwt (= 2.5.0) 278 | listen 279 | pry 280 | pry-byebug 281 | puma 282 | rails (= 7.0.8.4) 283 | rubocop-rails (= 2.15.0) 284 | sass-rails (~> 5.0) 285 | secure_headers (= 6.3.1) 286 | spring 287 | spring-watcher-listen 288 | turbolinks (~> 5) 289 | tzinfo-data 290 | uglifier (>= 1.3.0) 291 | web-console (>= 3.3.0) 292 | 293 | BUNDLED WITH 294 | 2.4.20 295 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Themis Solutions Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Third-Party Application 2 | 3 | **By using this sample code, you acknowledge that you have read, accepted and agree to be bound by and comply with the terms and conditions set out in the [Clio Developer Terms of Service](https://www.clio.com/partnerships/developers/developer-terms-service/).** 4 | 5 | This is an example third-party application that demonstrates the ability to use single sign-on (SSO) with Clio's authentication services. 6 | 7 | SSO makes it easy for Clio users to securely sign in and access your application at the click of a button using their Clio credentials, as opposed to having them create entirely new accounts and credentials for each third-party application they wish to utilize. 8 | 9 | Also included in this example application is making use of the access token obtained during SSO to make a successful API request. 10 | 11 | Note that this example code isn't in an ideal, refactored state. We've taken a "show your work" approach, clearly identifying the steps taken. In line with this we've also opted not to use some Ruby gems that might hide implementation details (such as `oauth2` that can automate part of the SSO flow for you). 12 | 13 | ## Prerequisites 14 | 15 | Whether you're a private developer or a company looking to integrate with Clio, the first step is to complete our [registration form](https://www.clio.com/partnerships/developers/get-started/). If you're looking to offer your integration to Clio customers, keep an eye out for an automatic follow-up email with instructions to setup your Developer Account on Clio Manage and Clio Identity. 16 | 17 | If you're already a Clio customer and are just looking to use our API for private use, you can get started right away. 18 | 19 | ### Clio Manage API Keys 20 | 21 | Clio Manage is the cloud-based legal practice management software that makes running a firm, organizing cases, and collaborating with clients from one place possible. 22 | 23 | Once your Developer Account has been set up, navigate to your [Developer Portal](http://app.clio.com/settings/developer_applications) within Clio Manage and follow our [instructions for creating a Clio application](https://app.clio.com/api/v4/documentation#section/Authorization-with-OAuth-2.0/Create-a-Clio-Application) on our Clio Manage API Documentation. Creating a Clio application will generate an application key and secret, which will be used to authorize your application with Clio and communicate with our Clio Manage API. 24 | 25 | ### Clio Identity API Keys 26 | 27 | Clio Identity is Clio's authentication service and identity provider. Similar to how you may see a "Login with GitHub" or "Login with Google" button when authenticating with an application on the web, third-party developers can leverage Clio Identity as an identity provider, cutting the time needed to write and maintain an authentication system for an application. 28 | 29 | To request a Clio Identity API key in order to implement SSO see our guide for [integrating with Clio Identity](https://developers.support.clio.com/hc/en-us/articles/4405288237723-Integrating-with-Clio-Identity-Single-Sign-on-with-Clio-). 30 | 31 | ### Setup 32 | 33 | First, clone the example application repository to your local environment: `git clone https://github.com/clio/example-third-party-application.git`. 34 | 35 | Then update the environment variables in `.env`: 36 | 1. Update the `CLIO_MANAGE_CLIENT_ID` and `CLIO_MANAGE_CLIENT_SECRET` values with your Clio Manage API Key and Secret 37 | 2. Update the `CLIO_MANAGE_SITE_URL` value with the region specific URL of your Clio Manage account: 38 | * `https://app.clio.com/` for the United States 39 | * `https://ca.app.clio.com/` for Canada 40 | * `https://eu.app.clio.com/` for Europe 41 | 3. Update the `CLIO_IDENTITY_CLIENT_ID` and `CLIO_IDENTITY_CLIENT_SECRET` values with your Clio Identity API Key and Secret 42 | 43 | You can then choose between a manual setup (install Ruby and Rails locally) or a docker setup (install Docker only). Manual setup instructions can be found in [docs/manual_setup.md](/docs/manual_setup.md) and the Docker setup instructions can be found in [docs/docker_setup.md](/docs/docker_setup.md). 44 | 45 | ## Example Entry Point 46 | 47 | ### SSO 48 | 49 | Both authentication with Clio Identity and authorization with Clio Manage start their flows in the `ApplicationController` (`app/controllers/application_controller.rb`). This file takes you through the steps outlined in our [Integrating with Clio Identity](https://developers.support.clio.com/hc/en-us/articles/4405288237723-Integrating-with-Clio-Identity-Single-Sign-on-with-Clio-) documentation (for performing authentication) and our [API Documentation](https://app.clio.com/api/v4/documentation#section/Authorization-with-OAuth-2.0/Obtaining-Authorization) (for obtaining authorization). 50 | 51 | ### API Requests 52 | 53 | The `ProfileController` (`app/controllers/profile_controller.rb`) uses the Clio Manage access token to make a simple API request to the `api/v4/users/who_am_i` endpoint. 54 | 55 | Meanwhile the `MatterController` (`app/controllers/matter_controller.rb`) uses the Clio Manage access token to make an API request to the `api/v4/matters` endpoint using [Unlimited Cursor Pagination](https://app.clio.com/api/v4/documentation#section/Paging/Unlimited-Cursor-Pagination). 56 | 57 | ## Support 58 | 59 | For technical inquiries, visit our [Clio Developers Help Center](https://developers.support.clio.com/hc/en-us), send us an email at api@clio.com, or visit our community-driven Slack workspace at [clio-public.slack.com](https://join.slack.com/t/clio-public/shared_invite/zt-qgkswsia-fyTprPgHGBR0TLAk7PENJw). 60 | 61 | For business and partnership inquiries, visit [https://www.clio.com/partnerships/developers/](https://www.clio.com/partnerships/developers/). 62 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require_relative "config/application" 2 | 3 | Rails.application.load_tasks 4 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | 7 | (function () { 8 | this.App || (this.App = {}); 9 | App.cable = ActionCable.createConsumer(); 10 | }.call(this)); 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/welcome.js: -------------------------------------------------------------------------------- 1 | function receiveMessageFromPopup(event) { 2 | if (!(event.origin === "http://localhost:3013" || event.origin === "https://example-third-party-application.clio.dev") || !event.isTrusted) { 3 | console.log("You are not worthy, received message from unknown source!"); 4 | } else { 5 | if (event.data === "authentication_successful") { 6 | console.log( 7 | "Sign in was successful, redirect to the matter page.", 8 | event.data 9 | ); 10 | window.location.href = "/matter"; 11 | } else { 12 | console.log("Sign in failed.", event.data); 13 | } 14 | } 15 | } 16 | 17 | //Listen for message events 18 | window.addEventListener("message", receiveMessageFromPopup, false); 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 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 file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's 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/SCSS 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 | body { 18 | font-size: 18px; 19 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 20 | margin: 0 auto; 21 | background: #F2F3F5; 22 | } 23 | 24 | .container { 25 | margin: 0 auto; 26 | padding: 25px; 27 | margin-bottom: 20px; 28 | max-width: 768px; 29 | } 30 | 31 | .card { 32 | background: #fff; 33 | border-radius: 5px; 34 | border: 1px solid #D7DEE2; 35 | -webkit-box-shadow: 0px 0px 12px -2px rgba(0,0,0,0.25); 36 | -moz-box-shadow: 0px 0px 12px -2px rgba(0,0,0,0.25); 37 | box-shadow: 0px 0px 12px -2px rgba(0,0,0,0.25); 38 | 39 | &.card-login { 40 | max-width: 438px; 41 | margin: 0 auto; 42 | } 43 | 44 | .card-header { 45 | font-weight: 700; 46 | padding: 15px 25px; 47 | border-bottom: 1px solid #D7DEE2; 48 | display: flex; 49 | justify-content: space-between; 50 | align-items: center; 51 | } 52 | 53 | .card-body { 54 | padding: 35px 25px; 55 | 56 | h1, h4 { 57 | margin-top: 0px; 58 | } 59 | 60 | div { 61 | margin-top: 30px; 62 | } 63 | 64 | .data-table { 65 | border-collapse: collapse; 66 | width: 100%; 67 | 68 | tr { 69 | height: 60px; 70 | } 71 | 72 | tr:nth-child(even) { 73 | background-color: #d8f6fc; 74 | } 75 | } 76 | } 77 | 78 | .card-footer { 79 | border-top: 1px solid #D7DEE2; 80 | padding: 15px 25px; 81 | 82 | p { 83 | color: #576C82; 84 | font-weight: 200; 85 | line-height: 1.4; 86 | font-size: 16px; 87 | margin: 8px 0; 88 | } 89 | } 90 | } 91 | 92 | form { 93 | .input-group { 94 | display: flex; 95 | flex-direction: column; 96 | margin-bottom: 22px; 97 | } 98 | } 99 | 100 | label { 101 | font-size: 13px; 102 | font-weight: 300; 103 | } 104 | 105 | input { 106 | border-radius: 3px; 107 | padding: 2px 12px; 108 | margin-top: 5px; 109 | border: 1px solid #D7DEE2; 110 | height: 32px; 111 | transition: border 0.1s ease-in-out; 112 | 113 | &:focus { 114 | border: 1px solid #0075bb; 115 | outline: none; 116 | box-shadow: none; 117 | transition: border 0.1s ease-in-out; 118 | } 119 | } 120 | 121 | p { 122 | color: #263238; 123 | font-weight: 400; 124 | line-height: 1.4; 125 | font-size: 16px; 126 | margin: 8px 0; 127 | } 128 | 129 | pre { 130 | background-color: #f5f7f8; 131 | padding: 16px; 132 | border-radius: 5px; 133 | overflow: auto; 134 | } 135 | 136 | .divider { 137 | border-right: 1px solid black; 138 | height: 110px; 139 | margin: 0 20px; 140 | } 141 | 142 | .text-center { 143 | text-align: center; 144 | } 145 | -------------------------------------------------------------------------------- /app/assets/stylesheets/buttons.scss: -------------------------------------------------------------------------------- 1 | // SSO Button CSS from the Styleguide 2 | // https://developers.support.clio.com/hc/en-us/articles/4405337190683--Sign-in-with-Clio-Button-guidelines-for-applications 3 | :root { 4 | // Variables overridden for Switchboard theming 5 | --text: #ffffff; 6 | --font: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | --default-state-background: #10b9df; 8 | --default-state-border: #0c91af; 9 | --hover-state-background: var(--default-state-border); 10 | --hover-state-underline: #ffffff; 11 | --focus-state-shadow: #004670; 12 | --focus-state-border: #ffffff; 13 | } 14 | 15 | button, a.button { 16 | height: 32px; 17 | padding: 8px 12px; 18 | border-radius: 4px; 19 | border: 1px solid var(--default-state-border); 20 | background-color: var(--default-state-background); 21 | background-image: linear-gradient(-180deg, var(--default-state-background) 0%, var(--default-state-border) 100%); 22 | font-family: var(--font); 23 | color: var(--text); 24 | font-size: 13px; 25 | font-style: normal; 26 | font-weight: bold; 27 | align-items: center; 28 | display: flex; 29 | position: relative; 30 | box-sizing: border-box; 31 | 32 | &:hover, &:focus { 33 | background-image: linear-gradient(-180deg, var(--hover-state-background) 0%, var(--hover-state-background) 100%); 34 | } 35 | 36 | &:hover:before { 37 | content: ""; 38 | border-radius: 0 0 1px 1px; 39 | border-bottom: 2px solid var(--hover-state-underline); 40 | position: absolute; 41 | bottom: 1px; 42 | left: 1px; 43 | right: 1px; 44 | } 45 | 46 | &:focus:after { 47 | box-shadow: 0 0 0 2px var(--focus-state-shadow); 48 | content: ""; 49 | border-radius: 4px; 50 | border: 1px solid var(--focus-state-border); 51 | position: absolute; 52 | top: -2px; 53 | bottom: -2px; 54 | left: -2px; 55 | right: -2px; 56 | } 57 | 58 | img { 59 | padding-right: 8px; 60 | height: 16px; 61 | width: 16px; 62 | } 63 | 64 | // Custom styles 65 | width: -moz-available; 66 | width: -webkit-fill-available; 67 | justify-content: center; 68 | } 69 | 70 | a.button { 71 | display: -webkit-box; 72 | display: -ms-flexbox; 73 | display: inline-flex; 74 | -webkit-box-align: center; 75 | -ms-flex-align: center; 76 | -ms-flex-pack: distribute; 77 | justify-content: center; 78 | -webkit-box-flex: 0; 79 | -ms-flex: 0 0 auto; 80 | -webkit-box-sizing: border-box; 81 | margin: 0; 82 | cursor: pointer; 83 | text-decoration: none; 84 | transition: all 0.1s ease-in-out; 85 | } 86 | 87 | // Spacing for the pagination buttons 88 | .pagination { 89 | display: flex; 90 | justify-content: space-between; 91 | width: 100%; 92 | 93 | div { 94 | margin-top: 0px !important; 95 | } 96 | 97 | .button { 98 | &.previous { 99 | width: 200px; 100 | } 101 | 102 | &.next { 103 | width: 200px; 104 | } 105 | } 106 | } 107 | 108 | .right { 109 | text-align: right; 110 | 111 | .button { 112 | width: 200px; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/assets/stylesheets/nav_menu.scss: -------------------------------------------------------------------------------- 1 | .menu-icon { 2 | position: relative; 3 | display: inline-block; 4 | width: 1.25em; 5 | height: 0.8em; 6 | margin-right: 0.3em; 7 | border-top: 0.2em solid #fff; 8 | border-bottom: 0.2em solid #fff; 9 | } 10 | 11 | .menu-icon:before { 12 | content: ""; 13 | position: absolute; 14 | top: 0.3em; 15 | left: 0px; 16 | width: 100%; 17 | border-top: 0.2em solid #fff; 18 | } 19 | 20 | .nav-dropdown { 21 | float: left; 22 | overflow: hidden; 23 | 24 | .nav-dropdown-content { 25 | display: none; 26 | position: absolute; 27 | background-color: #10b9df; 28 | background-image: linear-gradient(-180deg, #0c91af 0%, #10b9df 100%); 29 | border-radius: 3px; 30 | z-index: 1; 31 | 32 | a { 33 | color: #FFFFFF; 34 | padding: 12px 16px; 35 | text-decoration: none; 36 | display: block; 37 | } 38 | 39 | a:hover { 40 | background-color: #0c91af; 41 | background-image: linear-gradient(-180deg, #0c91af 0%, #0c91af 100%); 42 | } 43 | 44 | a:hover, a:active { 45 | text-decoration: underline #FFFFFF solid 2px; 46 | } 47 | 48 | a:active { 49 | border: 2px solid #004670; 50 | } 51 | } 52 | } 53 | 54 | .nav-dropdown:hover .nav-dropdown-content { 55 | display: block; 56 | } -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | require "http" 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | 6 | include IdentityAuthenticationHelper 7 | include ManageAuthorizationHelper 8 | 9 | # This method is performing the steps under "Obtaining Authorization" in the Integrating with Clio Identity documentation: 10 | # https://developers.support.clio.com/hc/en-us/articles/4405288237723-Integrating-with-Clio-Identity-Single-Sign-on-with-Clio- 11 | # 12 | # This will send the user to the Clio Identity Account page where they will log in and be given the option to grant 13 | # your application permission to access their Clio Identity information. After they have allowed access they will be 14 | # redirected to the specified redirect URI in this request. In this example that is `identity_callback` seen below. 15 | def authenticate_with_identity 16 | identity_state = SecureRandom.hex 17 | cookies.encrypted[:identity_state] = { 18 | value: identity_state, 19 | same_site: "None", 20 | secure: true, 21 | } 22 | 23 | redirect_to identity_authenticate_url(identity_state), allow_other_host: true 24 | end 25 | 26 | # This method is performing the steps under "Grant Approved" in the Integrating with Clio Identity documentation: 27 | # https://developers.support.clio.com/hc/en-us/articles/4405288237723-Integrating-with-Clio-Identity-Single-Sign-on-with-Clio- 28 | def identity_callback 29 | # First we ensure that the previous step was successful: the state is the same as what we sent in the previous 30 | # request and that we did successfully obtain a code. 31 | if !identity_state_valid?(params[:state]) 32 | render_identity_error("bad state", params.to_unsafe_h) 33 | return 34 | end 35 | if !params.has_key?(:code) 36 | render_identity_error("missing code", params.to_unsafe_h) 37 | return 38 | end 39 | 40 | # Then we make a request for the Clio Identity related information from the users account. 41 | response = HTTP.post(identity_token_url, form: identity_token_body(params[:code])) 42 | parsed_response = JSON.parse(response.body) 43 | 44 | # Make sure to handle any error cases, such as if authentication fails or the user revokes 45 | # your applications access grant. 46 | if response.code != 200 47 | render_identity_error("error getting id token", parsed_response) 48 | return 49 | end 50 | 51 | # The response has to be decoded (using the RS256 algorithm) and then validated (following 52 | # the Open ID specs). 53 | decoded_token = decode_and_validate_identity_token(parsed_response["id_token"]) 54 | 55 | # If validation passes, save the `id_token` for later use and move on to requesting 56 | # authorization in Manage. 57 | cookies.encrypted[:id_token] = { 58 | value: decoded_token, 59 | same_site: "None", 60 | secure: true, 61 | } 62 | authorize_with_manage 63 | rescue JwtDecodeError => e 64 | render_identity_error("JWT decode error", e.message) 65 | rescue JsonWebKeyError => e 66 | render_identity_error("error getting JSON web keys", JSON.parse(e.message)) 67 | rescue InvalidTokenError => e 68 | render_identity_error("invalid token", e.message) 69 | end 70 | 71 | # This method is performing the steps in step 1 of "Grant Type: Authorization Code" under "Obtaining Authorization" 72 | # in the Clio API Documentation: https://app.clio.com/api/v4/documentation#section/Authorization-with-OAuth-2.0/Obtaining-Authorization 73 | # 74 | # This will send the user to the Clio Manage Authorization page where they will log in and be given the option to 75 | # grant your application permission to access their Clio Manage information. After they have allowed access they will 76 | # be redirected to the specified redirect URI in this request. In this example that is `manage_callback` seen below. 77 | def authorize_with_manage 78 | manage_state = SecureRandom.hex 79 | cookies.encrypted[:manage_state] = { 80 | value: manage_state, 81 | same_site: "None", 82 | secure: true, 83 | } 84 | redirect_to manage_authorize_url(manage_state), allow_other_host: true 85 | end 86 | 87 | # This method is performing the steps in step 2 of "Grant Type: Authorization Code" under "Obtaining Authorization" 88 | # in the Clio API Documentation: https://app.clio.com/api/v4/documentation#section/Authorization-with-OAuth-2.0/Obtaining-Authorization 89 | def manage_callback 90 | # If `redirect_on_decline` was set to `true` in the first request we can verify that permission was granted in 91 | # the first step of Manage authorization. Here we also ensure that the state is the same as what we sent and 92 | # that we did successfully obtain a code. 93 | if params.has_key?(:error) 94 | render_manage_error(params[:error].sub("_", " "), params.to_unsafe_h) 95 | return 96 | end 97 | if !manage_state_valid?(params[:state]) 98 | render_manage_error("bad state", params.to_unsafe_h) 99 | return 100 | end 101 | if !params.has_key?(:code) 102 | render_manage_error("missing code", params.to_unsafe_h) 103 | return 104 | end 105 | 106 | # Then we make a request for the Clio Manaage access token. 107 | response = HTTP.headers("Content-Type" => "application/x-www-form-urlencoded").post(manage_token_url, form: manage_token_body(params[:code])) 108 | parsed_response = JSON.parse(response.body) 109 | 110 | # Make sure to handle any error cases. 111 | if response.code != 200 112 | render_manage_error("error getting access token", parsed_response) 113 | return 114 | end 115 | 116 | # And save the `access_token` for future Manage API requests. Access tokens have a limited lifespan and will 117 | # eventually expire. The expiry date and time can be calulcated with the `expires_in` value from the response 118 | # (which is the lifepsan in seconds). You may want to save the `refresh_token` to refresh your access once the 119 | # access token expires. More information on refresh tokens can be found in the Clio API Documentation: 120 | # https://app.clio.com/api/v4/documentation#section/Authorization-with-OAuth-2.0/Oauth-Refresh-Tokens 121 | cookies.encrypted[:manage_token] = { 122 | value: parsed_response["access_token"], 123 | same_site: "None", 124 | secure: true, 125 | } 126 | 127 | # Now your user is fully authenticated and authorized! 128 | # 129 | # When a user adds your app to Clio Manage from the App Directory you are required to redirect them to a 130 | # Clio Manage callback page (not your applications dashboard). This ensures that the user returns back to 131 | # the App Directory where they started. At this point in time, after the authentication and authorization 132 | # is complete, you'd create their account within your application before issuing this redirect. You can 133 | # read more about this process in this support article: 134 | # https://developers.support.clio.com/hc/en-us/articles/4405283081627 135 | # 136 | # Rather than implementing SSO twice, once for the usual SSO login flow and once for the "Add to Clio" flow 137 | # from the App Directory, we use a query parameter and cookie to distinguish between the two. The "Add to Clio" 138 | # URL we would provide for this example app would look something like https://myapp.com/welcome?install_flow=1. 139 | # On the welcome page this query parameter is saved in a cookie and here we use it to determine next steps. 140 | # 141 | # If we are performing the "Add to Clio" flow we need to create the user and their account within our 142 | # application then redirect to the Clio Manage callback page. If we performing the usual SSO login flow 143 | # we need to redirect them to our apps dashboard, which in this example is the profile page. 144 | if cookies.encrypted[:install_flow] == "1" 145 | # Create user/account in your application here 146 | redirect_to ENV["CLIO_MANAGE_SITE_URL"] + "app_integrations_callback", allow_other_host: true 147 | else 148 | redirect_to "/auth_popup_callback" 149 | end 150 | end 151 | 152 | # Upon succesful callback, we send a message to the main window 153 | def auth_popup_callback 154 | render plain: "", content_type: "text/html" 155 | end 156 | 157 | def signout 158 | cookies.delete :manage_token 159 | cookies.delete :id_token 160 | cookies.delete :identity_state 161 | cookies.delete :manage_state 162 | redirect_to root_path 163 | end 164 | 165 | private 166 | 167 | def render_identity_error(message, info) 168 | render_error("Clio Identity authentication failed: " + message, info) 169 | end 170 | 171 | def render_manage_error(message, info) 172 | render_error("Clio Manage authorization failed: " + message, info) 173 | end 174 | 175 | def render_error(message, info) 176 | render "error", locals: { 177 | error_message: message, 178 | error_info: info 179 | } 180 | end 181 | 182 | def require_manage_token 183 | # Ensure we still have the tokens obtained during authentication and authorization. 184 | @id_token = cookies.encrypted[:id_token] 185 | @manage_token = cookies.encrypted[:manage_token] 186 | 187 | if @id_token.blank? || @manage_token.blank? 188 | redirect_to root_path 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /app/controllers/matter_controller.rb: -------------------------------------------------------------------------------- 1 | require "http" 2 | 3 | class MatterController < ApplicationController 4 | 5 | before_action :require_manage_token 6 | 7 | def index 8 | # In this example we're getting the firms Matters using Unlimited Cursor Pagination 9 | # https://app.clio.com/api/v4/documentation#operation/Matter#index 10 | # https://app.clio.com/api/v4/documentation#section/Paging/Unlimited-Cursor-Pagination 11 | params = { 12 | fields: "id,display_number,description,status", 13 | status: "open", 14 | limit: 10, 15 | order: "id(asc)" 16 | } 17 | response = HTTP.auth("Bearer " + @manage_token).get(ENV["CLIO_MANAGE_SITE_URL"] + "api/v4/matters?" + params.to_query) 18 | 19 | handle_matters_response(response) 20 | end 21 | 22 | def paginate 23 | new_page_url = params[:page] 24 | response = HTTP.auth("Bearer " + @manage_token).get(new_page_url) 25 | handle_matters_response(response) 26 | end 27 | 28 | def show 29 | # In this example we're getting all attributes of a single Matter 30 | # https://app.clio.com/api/v4/documentation#operation/Matter#show 31 | matter_id = params[:id] 32 | if matter_id.blank? 33 | redirect_to root_path 34 | return 35 | end 36 | 37 | params = { 38 | fields: "id,etag,number,display_number,custom_number,description,status,location,client_reference,client_id,billable,maildrop_address,billing_method,open_date,close_date,pending_date,created_at,updated_at,shared,has_tasks,client,contingency_fee,custom_rate,evergreen_retainer,folder,group,matter_budget,originating_attorney,practice_area,responsible_attorney,statute_of_limitations,user,account_balances,custom_field_values,custom_field_set_associations,relationships,task_template_list_instances" 39 | } 40 | response = HTTP.auth("Bearer " + @manage_token).get(ENV["CLIO_MANAGE_SITE_URL"] + "api/v4/matters/" + matter_id + "?" + params.to_query) 41 | 42 | handle_matter_response(response) 43 | end 44 | 45 | private 46 | 47 | def handle_matters_response(response) 48 | parsed_response = JSON.parse(response) 49 | 50 | if response.code != 200 51 | render_error("Error getting matters response", parsed_response["error"]) 52 | return 53 | end 54 | 55 | render "index", locals: { 56 | paging: parsed_response["meta"]["paging"], 57 | matters: parsed_response["data"] 58 | } 59 | end 60 | 61 | def handle_matter_response(response) 62 | parsed_response = JSON.parse(response) 63 | 64 | if response.code != 200 65 | render_error("Error getting single matter response", parsed_response["error"]) 66 | return 67 | end 68 | 69 | render "show", locals: { 70 | matter: parsed_response["data"] 71 | } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /app/controllers/profile_controller.rb: -------------------------------------------------------------------------------- 1 | require "http" 2 | 3 | class ProfileController < ApplicationController 4 | 5 | before_action :require_manage_token 6 | 7 | def index 8 | # Then use the Clio Manage Access Token to make an API call. In this example we're getting some basic profile 9 | # information from the `api/v4/users/who_am_i` endpoint and displaying it on our profile page. 10 | params = { 11 | fields: "id,first_name,last_name,email,phone_number,account_owner" 12 | } 13 | 14 | response = HTTP.auth("Bearer " + @manage_token).get(ENV["CLIO_MANAGE_SITE_URL"] + "api/v4/users/who_am_i?" + params.to_query) 15 | parsed_response = JSON.parse(response) 16 | 17 | if response.code != 200 18 | render_error("Error getting who_am_i? response", parsed_response["error"]) 19 | return 20 | end 21 | 22 | render "index", locals: { 23 | data: parsed_response["data"] 24 | } 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class WelcomeController < ApplicationController 2 | 3 | def index 4 | cookies.encrypted[:install_flow] = params[:install_flow] 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/identity_authentication_helper.rb: -------------------------------------------------------------------------------- 1 | module IdentityAuthenticationHelper 2 | class JsonWebKeyError < StandardError; end 3 | class JwtDecodeError < StandardError; end 4 | class InvalidTokenError < StandardError; end 5 | 6 | def identity_authenticate_url(state) 7 | params = { 8 | response_type: "code", 9 | client_id: ENV["CLIO_IDENTITY_CLIENT_ID"], 10 | redirect_uri: ENV["ROOT_URL"] + "identity_callback", 11 | scope: "openid", 12 | state: state 13 | } 14 | ENV["CLIO_IDENTITY_SITE_URL"] + "oauth2/auth?" + params.to_query 15 | end 16 | 17 | def identity_token_url 18 | ENV["CLIO_IDENTITY_SITE_URL"] + "oauth2/token" 19 | end 20 | 21 | def identity_token_body(code) 22 | { 23 | client_id: ENV["CLIO_IDENTITY_CLIENT_ID"], 24 | client_secret: ENV["CLIO_IDENTITY_CLIENT_SECRET"], 25 | grant_type: "authorization_code", 26 | code: code, 27 | redirect_uri: ENV["ROOT_URL"] + "identity_callback" 28 | } 29 | end 30 | 31 | def identity_state_valid?(identity_state) 32 | identity_state.present? && (identity_state == cookies.encrypted[:identity_state]) 33 | end 34 | 35 | def get_json_web_keys 36 | @public_keys ||= begin 37 | response = HTTP.get(ENV["CLIO_IDENTITY_SITE_URL"] + ".well-known/jwks.json") 38 | 39 | if response.code != 200 40 | raise JsonWebKeyError.new(response.body.to_s) 41 | end 42 | 43 | JSON.parse(response.body)["keys"] 44 | end 45 | end 46 | 47 | def decode_and_validate_identity_token(identity_token) 48 | json_web_keys = get_json_web_keys 49 | 50 | decode_options = { 51 | algorithm: "RS256", 52 | iss: ENV["CLIO_IDENTITY_SITE_URL"], 53 | verify_iss: true, 54 | aud: [ENV["CLIO_IDENTITY_CLIENT_ID"]], 55 | verify_aud: true, 56 | verify_iat: true, 57 | jwks: { 58 | keys: json_web_keys.map(&:symbolize_keys) 59 | } 60 | } 61 | 62 | decoded_token = JWT.decode(identity_token, nil, true, decode_options) 63 | 64 | if identity_token_valid?(decoded_token[0]) 65 | decoded_token 66 | else 67 | raise InvalidTokenError.new("token missing sid or has invalid azp value") 68 | end 69 | rescue JWT::DecodeError => e 70 | raise JwtDecodeError.new(e.message) 71 | end 72 | 73 | def identity_token_valid?(token) 74 | token["sid"].present? && 75 | (!token["azp"].present? || token["azp"] == ENV["CLIO_IDENTITY_CLIENT_ID"]) 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /app/helpers/manage_authorization_helper.rb: -------------------------------------------------------------------------------- 1 | module ManageAuthorizationHelper 2 | 3 | def manage_authorize_url(state) 4 | params = { 5 | response_type: "code", 6 | client_id: ENV["CLIO_MANAGE_CLIENT_ID"], 7 | redirect_uri: ENV["ROOT_URL"] + "manage_callback", 8 | state: state, 9 | redirect_on_decline: true 10 | } 11 | ENV["CLIO_MANAGE_SITE_URL"] + "oauth/authorize?" + params.to_query 12 | end 13 | 14 | def manage_token_url 15 | ENV["CLIO_MANAGE_SITE_URL"] + "oauth/token?" 16 | end 17 | 18 | def manage_token_body(code) 19 | { 20 | client_id: ENV["CLIO_MANAGE_CLIENT_ID"], 21 | client_secret: ENV["CLIO_MANAGE_CLIENT_SECRET"], 22 | grant_type: "authorization_code", 23 | code: code, 24 | redirect_uri: ENV["ROOT_URL"] + "manage_callback" 25 | } 26 | end 27 | 28 | def manage_state_valid?(manage_state) 29 | manage_state.present? && manage_state == cookies.encrypted[:manage_state] 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/views/application/error.html.erb: -------------------------------------------------------------------------------- 1 |
<%= error_message %>
11 | 12 | <% if error_info.present? %> 13 |<%= JSON.pretty_generate(error_info) %>15 | <% end %> 16 |
Display Number | 21 |Description | 22 |Status | 23 |24 | |
---|---|---|---|
<%= matter["display_number"] %> | 28 |<%= matter["description"] %> | 29 |<%= matter["status"] %> | 30 |31 | " class="button"> 32 | View 33 | 34 | | 35 |
<%= attribute_key %> | 22 |23 | <% if value.is_a?(Hash) %> 24 | 25 | <% value.each do |sub_attribute_key, sub_value| %> 26 | <%= sub_attribute_key %>: <%= sub_value %> 27 | <% end %> 28 | 29 | <% elsif value.is_a?(Array) %> 30 | 31 | <% value.each do |object| %> 32 | <% object.each do |sub_attribute_key, sub_value| %> 33 | <%= sub_attribute_key %>: <%= sub_value %> 34 | <% end %> 35 | 36 | <% end %> 37 | <% else %> 38 | <%= value %> 39 | <% end %> 40 | | 41 |
---|
Hi <%= data["first_name"] %>! You have logged in to this example third-party application with single sign-on using your Clio account!
19 | 20 |<%= JSON.pretty_generate(cookies.encrypted[:id_token]) %>23 |
<%= cookies.encrypted[:manage_token] %>28 |
<%= JSON.pretty_generate(data) %>33 |
You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |