ditty 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }