ditty 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.pryrc +6 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +15 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile.ci +19 -0
  8. data/License.txt +7 -0
  9. data/Rakefile +10 -0
  10. data/Readme.md +67 -0
  11. data/config.ru +33 -0
  12. data/ditty.gemspec +46 -0
  13. data/lib/ditty/components/app.rb +78 -0
  14. data/lib/ditty/controllers/application.rb +79 -0
  15. data/lib/ditty/controllers/audit_logs.rb +44 -0
  16. data/lib/ditty/controllers/component.rb +161 -0
  17. data/lib/ditty/controllers/main.rb +86 -0
  18. data/lib/ditty/controllers/roles.rb +16 -0
  19. data/lib/ditty/controllers/users.rb +183 -0
  20. data/lib/ditty/db.rb +12 -0
  21. data/lib/ditty/helpers/authentication.rb +58 -0
  22. data/lib/ditty/helpers/component.rb +63 -0
  23. data/lib/ditty/helpers/pundit.rb +34 -0
  24. data/lib/ditty/helpers/views.rb +50 -0
  25. data/lib/ditty/helpers/wisper.rb +14 -0
  26. data/lib/ditty/listener.rb +23 -0
  27. data/lib/ditty/models/audit_log.rb +14 -0
  28. data/lib/ditty/models/base.rb +7 -0
  29. data/lib/ditty/models/identity.rb +70 -0
  30. data/lib/ditty/models/role.rb +16 -0
  31. data/lib/ditty/models/user.rb +63 -0
  32. data/lib/ditty/policies/application_policy.rb +21 -0
  33. data/lib/ditty/policies/audit_log_policy.rb +41 -0
  34. data/lib/ditty/policies/identity_policy.rb +25 -0
  35. data/lib/ditty/policies/role_policy.rb +41 -0
  36. data/lib/ditty/policies/user_policy.rb +47 -0
  37. data/lib/ditty/rake_tasks.rb +85 -0
  38. data/lib/ditty/seed.rb +1 -0
  39. data/lib/ditty/services/logger.rb +48 -0
  40. data/lib/ditty/version.rb +5 -0
  41. data/lib/ditty.rb +142 -0
  42. data/migrate/20170207_base_tables.rb +40 -0
  43. data/migrate/20170208_audit_log.rb +12 -0
  44. data/migrate/20170416_audit_log_details.rb +9 -0
  45. data/public/browserconfig.xml +9 -0
  46. data/public/images/apple-icon.png +0 -0
  47. data/public/images/favicon-16x16.png +0 -0
  48. data/public/images/favicon-32x32.png +0 -0
  49. data/public/images/launcher-icon-1x.png +0 -0
  50. data/public/images/launcher-icon-2x.png +0 -0
  51. data/public/images/launcher-icon-4x.png +0 -0
  52. data/public/images/mstile-150x150.png +0 -0
  53. data/public/images/safari-pinned-tab.svg +43 -0
  54. data/public/manifest.json +25 -0
  55. data/views/404.haml +7 -0
  56. data/views/audit_logs/index.haml +30 -0
  57. data/views/error.haml +4 -0
  58. data/views/identity/login.haml +19 -0
  59. data/views/identity/register.haml +14 -0
  60. data/views/index.haml +1 -0
  61. data/views/layout.haml +55 -0
  62. data/views/partials/delete_form.haml +4 -0
  63. data/views/partials/footer.haml +5 -0
  64. data/views/partials/form_control.haml +20 -0
  65. data/views/partials/navbar.haml +24 -0
  66. data/views/partials/notifications.haml +24 -0
  67. data/views/partials/pager.haml +14 -0
  68. data/views/partials/sidebar.haml +35 -0
  69. data/views/roles/display.haml +18 -0
  70. data/views/roles/edit.haml +11 -0
  71. data/views/roles/form.haml +1 -0
  72. data/views/roles/index.haml +22 -0
  73. data/views/roles/new.haml +10 -0
  74. data/views/users/display.haml +50 -0
  75. data/views/users/edit.haml +11 -0
  76. data/views/users/identity.haml +3 -0
  77. data/views/users/index.haml +23 -0
  78. data/views/users/new.haml +11 -0
  79. data/views/users/profile.haml +39 -0
  80. data/views/users/user.haml +3 -0
  81. metadata +431 -0
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/models/base'
4
+ require 'digest/md5'
5
+ require 'active_support'
6
+ require 'active_support/core_ext/object/blank'
7
+
8
+ # Why not store this in Elasticsearch?
9
+ module Ditty
10
+ class User < Sequel::Model
11
+ include ::Ditty::Base
12
+
13
+ one_to_many :identity
14
+ many_to_many :roles
15
+ one_to_many :audit_logs
16
+
17
+ def role?(check)
18
+ !roles_dataset.first(name: check).nil?
19
+ end
20
+
21
+ def method_missing(method_sym, *arguments, &block)
22
+ if method_sym.to_s[-1] == '?'
23
+ role?(method_sym[0..-2])
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def respond_to_missing?(name, _include_private = false)
30
+ name[-1] == '?'
31
+ end
32
+
33
+ def gravatar
34
+ hash = Digest::MD5.hexdigest(email.downcase)
35
+ "https://www.gravatar.com/avatar/#{hash}"
36
+ end
37
+
38
+ def validate
39
+ validates_presence :email
40
+ return if email.blank?
41
+ validates_unique :email
42
+ validates_format(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :email)
43
+ end
44
+
45
+ # Add the basic roles and identity
46
+ def after_create
47
+ check_roles
48
+ end
49
+
50
+ def check_roles
51
+ return if role?('anonymous')
52
+ add_role Role.find_or_create(name: 'user') unless role?('user')
53
+ end
54
+
55
+ def index_prefix
56
+ email
57
+ end
58
+
59
+ def username
60
+ identity_dataset.first.username
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ditty
4
+ class ApplicationPolicy
5
+ attr_reader :user, :record
6
+
7
+ def initialize(user, record)
8
+ @user = user
9
+ @record = record
10
+ end
11
+
12
+ class Scope
13
+ attr_reader :user, :scope
14
+
15
+ def initialize(user, scope)
16
+ @user = user
17
+ @scope = scope
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/policies/application_policy'
4
+
5
+ module Ditty
6
+ class AuditLogPolicy < ApplicationPolicy
7
+ def create?
8
+ user && user.super_admin?
9
+ end
10
+
11
+ def list?
12
+ create?
13
+ end
14
+
15
+ def read?
16
+ create?
17
+ end
18
+
19
+ def update?
20
+ read?
21
+ end
22
+
23
+ def delete?
24
+ create?
25
+ end
26
+
27
+ def permitted_attributes
28
+ %i[action details]
29
+ end
30
+
31
+ class Scope < ApplicationPolicy::Scope
32
+ def resolve
33
+ if user && user.super_admin?
34
+ scope
35
+ else
36
+ scope.where(id: -1)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/policies/application_policy'
4
+
5
+ module Ditty
6
+ class IdentityPolicy < ApplicationPolicy
7
+ def register?
8
+ true
9
+ end
10
+
11
+ def permitted_attributes
12
+ %i[username password password_confirmation]
13
+ end
14
+
15
+ class Scope < ApplicationPolicy::Scope
16
+ def resolve
17
+ if user.super_admin?
18
+ scope.all
19
+ else
20
+ []
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/policies/application_policy'
4
+
5
+ module Ditty
6
+ class RolePolicy < ApplicationPolicy
7
+ def create?
8
+ user && user.super_admin?
9
+ end
10
+
11
+ def list?
12
+ create?
13
+ end
14
+
15
+ def read?
16
+ create?
17
+ end
18
+
19
+ def update?
20
+ read?
21
+ end
22
+
23
+ def delete?
24
+ create?
25
+ end
26
+
27
+ def permitted_attributes
28
+ [:name]
29
+ end
30
+
31
+ class Scope < ApplicationPolicy::Scope
32
+ def resolve
33
+ if user && user.super_admin?
34
+ scope
35
+ else
36
+ scope.where(id: -1)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/policies/application_policy'
4
+
5
+ module Ditty
6
+ class UserPolicy < ApplicationPolicy
7
+ def create?
8
+ user && user.super_admin?
9
+ end
10
+
11
+ def list?
12
+ create?
13
+ end
14
+
15
+ def read?
16
+ user && (record.id == user.id || user.super_admin?)
17
+ end
18
+
19
+ def update?
20
+ read?
21
+ end
22
+
23
+ def delete?
24
+ create?
25
+ end
26
+
27
+ def register?
28
+ true
29
+ end
30
+
31
+ def permitted_attributes
32
+ attribs = %i[email name surname]
33
+ attribs << :role_id if user.super_admin?
34
+ attribs
35
+ end
36
+
37
+ class Scope < ApplicationPolicy::Scope
38
+ def resolve
39
+ if user && user.super_admin?
40
+ scope
41
+ else
42
+ scope.where(id: user.id)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+ require 'rake/tasklib'
5
+
6
+ module Ditty
7
+ class Tasks < ::Rake::TaskLib
8
+ include ::Rake::DSL if defined?(::Rake::DSL)
9
+
10
+ def install_tasks
11
+ namespace :ditty do
12
+ desc 'Generate the needed tokens'
13
+ task :generate_tokens do
14
+ puts 'Generating the Ditty tokens'
15
+ require 'securerandom'
16
+ File.write('.session_secret', SecureRandom.random_bytes(40)) unless File.file?('.session_secret')
17
+ File.write('.token_secret', SecureRandom.random_bytes(40)) unless File.file?('.token_secret')
18
+ end
19
+
20
+ desc 'Seed the Ditty database'
21
+ task :seed do
22
+ puts 'Seeding the Ditty database'
23
+ require 'ditty/seed'
24
+ end
25
+
26
+ desc 'Prepare Ditty migrations'
27
+ task :prep do
28
+ puts 'Prepare the Ditty folders'
29
+ Dir.mkdir 'pids' unless File.exist?('pids')
30
+
31
+ puts 'Preparing the Ditty migrations folder'
32
+ Dir.mkdir 'migrations' unless File.exist?('migrations')
33
+ ::Ditty::Components.migrations.each do |path|
34
+ FileUtils.cp_r "#{path}/.", 'migrations'
35
+ end
36
+ end
37
+
38
+ desc 'Migrate Ditty database to latest version'
39
+ task :migrate do
40
+ puts 'Running the Ditty migrations'
41
+ Rake::Task['ditty:migrate:up'].invoke
42
+ end
43
+
44
+ namespace :migrate do
45
+ folder = 'migrations'
46
+
47
+ desc 'Check if the migration is current'
48
+ task :check do
49
+ require 'sequel'
50
+ puts 'Running Ditty Migrations check'
51
+ ::Sequel.extension :migration
52
+ ::Sequel::Migrator.check_current(::DB, folder)
53
+ end
54
+
55
+ desc 'Migrate Ditty database to latest version'
56
+ task :up do
57
+ require 'sequel'
58
+ puts 'Running Ditty Migrations up'
59
+ ::Sequel.extension :migration
60
+ ::Sequel::Migrator.apply(::DB, folder)
61
+ end
62
+
63
+ desc 'Roll back the Ditty database'
64
+ task :down do
65
+ require 'sequel'
66
+ puts 'Running Ditty Migrations down'
67
+ ::Sequel.extension :migration
68
+ ::Sequel::Migrator.apply(::DB, folder, 0)
69
+ end
70
+
71
+ desc 'Reset the Ditty database'
72
+ task :bounce do
73
+ require 'sequel'
74
+ puts 'Running Ditty Migrations bounce'
75
+ ::Sequel.extension :migration
76
+ ::Sequel::Migrator.apply(::DB, folder, 0)
77
+ ::Sequel::Migrator.apply(::DB, folder)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ Ditty::Tasks.new.install_tasks
data/lib/ditty/seed.rb ADDED
@@ -0,0 +1 @@
1
+ ::Ditty::Components.seeders.each(&:call)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'yaml'
5
+ require 'singleton'
6
+ require 'active_support/inflector'
7
+
8
+ module Ditty
9
+ module Services
10
+ class Logger
11
+ include Singleton
12
+
13
+ CONFIG = './config/logger.yml'.freeze
14
+ attr_reader :loggers
15
+
16
+ def initialize
17
+ @loggers = []
18
+ config.each do |values|
19
+ klass = values['class'].constantize
20
+ opts = values['options'] || nil
21
+ logger = klass.new(opts)
22
+ if values['level']
23
+ logger.level = klass.const_get(values['level'].to_sym)
24
+ end
25
+ @loggers << logger
26
+ end
27
+ end
28
+
29
+ def method_missing(method, *args, &block)
30
+ loggers.each { |logger| logger.send(method, *args, &block) }
31
+ end
32
+
33
+ def respond_to_missing?(method, _include_private = false)
34
+ loggers.any? { |logger| logger.respond_to?(method) }
35
+ end
36
+
37
+ private
38
+
39
+ def config
40
+ @config ||= File.exist?(CONFIG) ? YAML.load_file(CONFIG) : default
41
+ end
42
+
43
+ def default
44
+ [{ 'name' => 'default', 'class' => 'Logger' }]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ditty
4
+ VERSION = '0.2.0'.freeze
5
+ end
data/lib/ditty.rb ADDED
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/version'
4
+
5
+ module Ditty
6
+ class ComponentError < StandardError; end
7
+
8
+ # A thread safe cache class, offering only #[] and #[]= methods,
9
+ # each protected by a mutex.
10
+ # Ripped off from Roda - https://github.com/jeremyevans/roda
11
+ class ComponentCache
12
+ # Create a new thread safe cache.
13
+ def initialize
14
+ @mutex = Mutex.new
15
+ @hash = {}
16
+ end
17
+
18
+ # Make getting value from underlying hash thread safe.
19
+ def [](key)
20
+ @mutex.synchronize { @hash[key] }
21
+ end
22
+
23
+ # Make setting value in underlying hash thread safe.
24
+ def []=(key, value)
25
+ @mutex.synchronize { @hash[key] = value }
26
+ end
27
+
28
+ def map(&block)
29
+ @mutex.synchronize { @hash.map(&block) }
30
+ end
31
+
32
+ def inject(memo, &block)
33
+ @mutex.synchronize { @hash.inject(memo, &block) }
34
+ end
35
+ end
36
+
37
+ # Ripped off from Roda - https://github.com/jeremyevans/roda
38
+ module Components
39
+ # Stores registered components
40
+ @components = ComponentCache.new
41
+
42
+ # If the registered component already exists, use it. Otherwise,
43
+ # require it and return it. This raises a LoadError if such a
44
+ # component doesn't exist, or a Component if it exists but it does
45
+ # not register itself correctly.
46
+ def self.load_component(name)
47
+ h = @components
48
+ unless (component = h[name])
49
+ require "ditty/components/#{name}"
50
+ raise ComponentError, "Component #{name} did not register itself correctly in Ditty::Components" unless (component = h[name])
51
+ end
52
+ component
53
+ end
54
+
55
+ # Register the given component with Component, so that it can be loaded using #component
56
+ # with a symbol. Should be used by component files. Example:
57
+ #
58
+ # Ditty::Components.register_component(:component_name, ComponentModule)
59
+ def self.register_component(name, mod)
60
+ puts "Registering #{mod} as #{name}"
61
+ @components[name] = mod
62
+ end
63
+
64
+ def self.components
65
+ @components
66
+ end
67
+
68
+ # Return a hash of controllers with their routes as keys: `{ '/users' => Ditty::Controllers::Users }`
69
+ def self.routes
70
+ @routes ||= {}
71
+ end
72
+
73
+ def self.routes=(routes)
74
+ @routes = routes
75
+ end
76
+
77
+ # Return an ordered list of navigation items:
78
+ # `[{order:0, link:'/users/', text:'Users'}, {order:1, link:'/roles/', text:'Roles'}]
79
+ def self.navigation
80
+ @navigation ||= []
81
+ end
82
+
83
+ def self.navigation=(navigation)
84
+ @navigation = navigation
85
+ end
86
+
87
+ def self.migrations
88
+ @migrations ||= []
89
+ end
90
+
91
+ def self.migrations=(migrations)
92
+ @migrations = migrations
93
+ end
94
+
95
+ def self.seeders
96
+ @seeders ||= []
97
+ end
98
+
99
+ def self.seeders=(seeders)
100
+ @seeders = seeders
101
+ end
102
+
103
+ def self.workers
104
+ @workers ||= []
105
+ end
106
+
107
+ def self.workers=(workers)
108
+ @workers = workers
109
+ end
110
+
111
+ module Base
112
+ module ClassMethods
113
+ # Load a new component into the current class. A component can be a module
114
+ # which is used directly, or a symbol represented a registered component
115
+ # which will be required and then used. Returns nil.
116
+ #
117
+ # Component.component ComponentModule
118
+ # Component.component :csrf
119
+ def component(component, *args, &block)
120
+ raise ComponentError, 'Cannot add a component to a frozen Component class' if frozen?
121
+ component = Components.load_component(component) if component.is_a?(Symbol)
122
+ include(component::InstanceMethods) if defined?(component::InstanceMethods)
123
+ extend(component::ClassMethods) if defined?(component::ClassMethods)
124
+
125
+ component.configure(self, *args, &block) if component.respond_to?(:configure)
126
+ Components.navigation.concat component.navigation if component.respond_to?(:navigation)
127
+ Components.routes.merge! component.routes if component.respond_to?(:routes)
128
+ Components.migrations << component.migrations if component.respond_to?(:migrations)
129
+ Components.seeders << component.seeder if component.respond_to?(:seeder)
130
+ Components.workers.concat component.workers if component.respond_to?(:workers)
131
+ nil
132
+ end
133
+ end
134
+
135
+ module InstanceMethods
136
+ end
137
+ end
138
+ end
139
+
140
+ extend Components::Base::ClassMethods
141
+ component Components::Base
142
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table :users do
6
+ primary_key :id
7
+ String :name
8
+ String :surname
9
+ String :email
10
+ DateTime :created_at
11
+ DateTime :updated_at
12
+ unique [:email]
13
+ end
14
+
15
+ create_table :identities do
16
+ primary_key :id
17
+ foreign_key :user_id, :users
18
+ String :username
19
+ String :crypted_password
20
+ DateTime :created_at
21
+ DateTime :updated_at
22
+ unique [:username]
23
+ end
24
+
25
+ create_table :roles do
26
+ primary_key :id
27
+ String :name
28
+ DateTime :created_at
29
+ DateTime :updated_at
30
+ unique [:name]
31
+ end
32
+
33
+ create_table :roles_users do
34
+ DateTime :created_at
35
+ foreign_key :user_id, :users
36
+ foreign_key :role_id, :roles
37
+ unique %i[user_id role_id]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table :audit_logs do
6
+ primary_key :id
7
+ foreign_key :user_id, :users, null: true
8
+ String :action
9
+ DateTime :created_at
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table :audit_logs do
6
+ add_column :details, String, text: true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <browserconfig>
3
+ <msapplication>
4
+ <tile>
5
+ <square150x150logo src="/images/mstile-150x150.png"/>
6
+ <TileColor>#da532c</TileColor>
7
+ </tile>
8
+ </msapplication>
9
+ </browserconfig>
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,43 @@
1
+ <?xml version="1.0" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
+ width="192.000000pt" height="192.000000pt" viewBox="0 0 192.000000 192.000000"
6
+ preserveAspectRatio="xMidYMid meet">
7
+ <metadata>
8
+ Created by potrace 1.11, written by Peter Selinger 2001-2013
9
+ </metadata>
10
+ <g transform="translate(0.000000,192.000000) scale(0.100000,-0.100000)"
11
+ fill="#000000" stroke="none">
12
+ <path d="M812 1896 c-3 -6 -7 -46 -7 -88 -1 -43 -4 -80 -8 -84 -3 -4 -26 -9
13
+ -49 -11 -97 -11 -247 -74 -343 -146 -324 -242 -395 -717 -159 -1050 19 -26 34
14
+ -49 34 -52 0 -2 -27 -32 -60 -65 -33 -33 -60 -63 -60 -67 0 -12 121 -119 175
15
+ -154 116 -76 273 -139 380 -153 61 -8 251 -9 285 -1 14 3 42 8 62 10 95 13
16
+ 280 99 381 178 182 142 294 322 346 554 31 136 27 266 -13 449 -9 41 -59 159
17
+ -94 219 -83 144 -209 268 -355 349 -71 40 -206 91 -263 101 -130 21 -242 26
18
+ -252 11z m240 -135 c151 -34 301 -120 411 -236 83 -87 184 -269 201 -360 3
19
+ -16 9 -55 15 -85 11 -64 7 -213 -7 -285 -61 -297 -283 -537 -577 -624 -86 -25
20
+ -122 -30 -216 -31 -188 -3 -347 48 -507 163 l-43 31 300 300 299 301 0 135 c0
21
+ 74 1 265 1 423 l1 289 38 -5 c20 -3 58 -11 84 -16z m-248 -256 l1 -88 -50 -14
22
+ c-295 -82 -433 -407 -285 -670 l30 -52 -60 -61 c-32 -33 -63 -60 -67 -60 -25
23
+ 1 -97 139 -124 236 -40 146 -19 336 52 463 91 163 220 265 404 319 99 30 99
24
+ 30 99 -73z m1 -361 c1 -76 -3 -147 -8 -158 -5 -10 -53 -63 -108 -117 l-99 -99
25
+ -15 24 c-70 106 -51 283 42 386 45 50 153 115 178 107 6 -2 10 -59 10 -143z"/>
26
+ <path d="M990 1600 l0 -30 168 0 167 0 -45 30 c-44 29 -48 30 -167 30 l-123 0
27
+ 0 -30z"/>
28
+ <path d="M990 1417 l0 -32 250 0 c138 0 250 3 250 6 0 4 -9 17 -19 31 l-20 25
29
+ -230 1 -231 1 0 -32z"/>
30
+ <path d="M994 1261 c-2 -2 -4 -16 -4 -31 l0 -27 299 0 300 0 -13 31 -12 31
31
+ -283 0 c-155 0 -284 -2 -287 -4z"/>
32
+ <path d="M994 1081 c-2 -2 -4 -17 -4 -33 l0 -28 315 0 315 0 0 30 c0 24 -4 30
33
+ -22 31 -116 3 -601 3 -604 0z"/>
34
+ <path d="M959 870 l-32 -30 39 -1 c132 -2 646 -1 649 2 1 2 5 15 7 29 l3 25
35
+ -317 3 -318 2 -31 -30z"/>
36
+ <path d="M777 690 c-15 -15 -27 -29 -27 -31 0 -2 183 -4 407 -4 l407 0 12 31
37
+ 13 31 -393 0 -393 0 -26 -27z"/>
38
+ <path d="M600 510 c-17 -14 -30 -28 -30 -32 0 -3 196 -6 436 -6 l437 0 23 24
39
+ c13 12 24 27 24 31 0 4 -194 8 -430 8 l-431 0 -29 -25z"/>
40
+ <path d="M430 341 c0 -5 16 -18 36 -30 34 -20 47 -21 402 -21 l367 0 45 30 45
41
+ 30 -447 0 c-260 0 -448 -4 -448 -9z"/>
42
+ </g>
43
+ </svg>
@@ -0,0 +1,25 @@
1
+ {
2
+ "short_name": "Ditty",
3
+ "name": "Ditty",
4
+ "icons": [
5
+ {
6
+ "src": "images/launcher-icon-1x.png",
7
+ "type": "image/png",
8
+ "sizes": "48x48"
9
+ },
10
+ {
11
+ "src": "images/launcher-icon-2x.png",
12
+ "type": "image/png",
13
+ "sizes": "96x96"
14
+ },
15
+ {
16
+ "src": "images/launcher-icon-4x.png",
17
+ "type": "image/png",
18
+ "sizes": "192x192"
19
+ }
20
+ ],
21
+ "start_url": "auth/identity",
22
+ "theme_color": "#ffffff",
23
+ "background_color": "#ffffff",
24
+ "display": "standalone"
25
+ }