darjeelink 0.11.5

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +82 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/config/darjeelink_manifest.js +2 -0
  6. data/app/assets/javascripts/darjeelink/application.js +15 -0
  7. data/app/assets/javascripts/darjeelink/sparkleh.js +248 -0
  8. data/app/assets/javascripts/darjeelink/tracking_link_generator.js +36 -0
  9. data/app/assets/stylesheets/darjeelink/application.css +15 -0
  10. data/app/controllers/darjeelink/application_controller.rb +31 -0
  11. data/app/controllers/darjeelink/sessions_controller.rb +31 -0
  12. data/app/controllers/darjeelink/short_links_controller.rb +111 -0
  13. data/app/controllers/darjeelink/tracking_links_controller.rb +8 -0
  14. data/app/helpers/darjeelink/application_helper.rb +6 -0
  15. data/app/importers/darjeelink/rebrandly_importer.rb +48 -0
  16. data/app/importers/darjeelink/short_link_importer.rb +32 -0
  17. data/app/jobs/darjeelink/application_job.rb +6 -0
  18. data/app/mailers/darjeelink/application_mailer.rb +8 -0
  19. data/app/models/darjeelink/application_record.rb +7 -0
  20. data/app/models/darjeelink/short_link.rb +58 -0
  21. data/app/views/darjeelink/short_links/_short_link_form.erb +24 -0
  22. data/app/views/darjeelink/short_links/edit.html.erb +29 -0
  23. data/app/views/darjeelink/short_links/index.html.erb +75 -0
  24. data/app/views/darjeelink/short_links/new.html.erb +12 -0
  25. data/app/views/darjeelink/tracking_links/new.erb +49 -0
  26. data/app/views/layouts/darjeelink/application.html.erb +39 -0
  27. data/config/initializers/darjeelink.rb +31 -0
  28. data/config/initializers/omniauth.rb +10 -0
  29. data/config/initializers/rebrandly.rb +5 -0
  30. data/config/routes.rb +11 -0
  31. data/db/migrate/20190103122918_create_portkey_short_links.rb +16 -0
  32. data/db/migrate/20190304094710_case_insensitive_index_short_link_shortened_path.rb +14 -0
  33. data/db/migrate/20200505220716_rename_portkey_short_links_to_darjeelink_short_links.rb +7 -0
  34. data/db/migrate/20200505221026_rename_portkey_short_link_index.rb +15 -0
  35. data/lib/darjeelink.rb +30 -0
  36. data/lib/darjeelink/configuration.rb +19 -0
  37. data/lib/darjeelink/engine.rb +23 -0
  38. data/lib/darjeelink/version.rb +5 -0
  39. data/lib/tasks/darjeelink.rake +6 -0
  40. metadata +250 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+
7
+ before_action :check_ip_whitelist
8
+ before_action :authenticate
9
+
10
+ private
11
+
12
+ # Check user IP address against whitelist from ENV
13
+ def check_ip_whitelist
14
+ return unless Rails.env.production?
15
+ return unless Rails.application.config.permitted_ips
16
+ return if Rails.application.config.permitted_ips.split(',').include? request.ip
17
+
18
+ render plain: 'Access Denied', status: :forbidden
19
+ end
20
+
21
+ # Authenticate against Google OAuth
22
+ def authenticate
23
+ return if session[:email] || request.path =~ /google_oauth2/ || Rails.env.test?
24
+ # Seperate line to make it easier to disable the bypass if doing some auth
25
+ # related locally
26
+ return if Rails.env.development?
27
+
28
+ redirect_post '/auth/google_oauth2'
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class SessionsController < Darjeelink::ApplicationController
5
+ def create
6
+ if auth_domain.present? && auth_domain == Darjeelink.auth_domain
7
+ update_session
8
+ redirect_to '/'
9
+ else
10
+ render plain: 'Access Denied', status: :forbidden
11
+ end
12
+ end
13
+
14
+ protected
15
+
16
+ def auth_hash
17
+ request.env['omniauth.auth']
18
+ end
19
+
20
+ def auth_domain
21
+ auth_hash['info']['email'].split('@')[1] if auth_hash
22
+ end
23
+
24
+ private
25
+
26
+ def update_session
27
+ session[:email] = auth_hash['info']['email']
28
+ session[:name] = auth_hash['info']['name']
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ShortLinksController < Darjeelink::ApplicationController
5
+ skip_before_action :check_ip_whitelist, only: :show
6
+ skip_before_action :authenticate, only: :show
7
+
8
+ before_action :check_url_present, only: :create
9
+ before_action :check_url_valid, only: :create
10
+
11
+ class ShortLinkNotFoundError < StandardError
12
+ end
13
+
14
+ def index
15
+ @short_links = Darjeelink::ShortLink.search(params[:query])
16
+ .order(id: 'desc')
17
+ .paginate(page: params[:page], per_page: 20)
18
+ end
19
+
20
+ def new
21
+ end
22
+
23
+ def create
24
+ begin
25
+ Darjeelink::ShortLink.create!(url: params[:url], shortened_path: params[:shortened_path])
26
+ rescue ActiveRecord::RecordNotUnique
27
+ flash[:error] = "#{params[:shortened_path]} already used! Choose a different custom path"
28
+ return redirect_to(darjeelink.new_short_link_path)
29
+ end
30
+
31
+ redirect_to(darjeelink.short_links_path)
32
+ end
33
+
34
+ def edit
35
+ @short_link = Darjeelink::ShortLink.find(params[:id])
36
+ end
37
+
38
+ def update
39
+ @short_link = Darjeelink::ShortLink.find(params[:id])
40
+ @short_link.update!(short_link_params)
41
+
42
+ redirect_to(darjeelink.short_links_path)
43
+ end
44
+
45
+ def show
46
+ shortened_path = CGI.escape(params[:id])
47
+ short_link = Darjeelink::ShortLink.ifind(shortened_path)
48
+
49
+ if short_link.nil?
50
+ log_missing_shortlink(shortened_path)
51
+ return redirect_to(Darjeelink.fallback_url)
52
+ end
53
+
54
+ short_link.update!(visits: short_link.visits + 1)
55
+ redirect_to(build_url(short_link.url))
56
+ end
57
+
58
+ private
59
+
60
+ def log_missing_shortlink(shortened_path)
61
+ Rails.logger.warn("ShortLink not found: #{shortened_path}")
62
+ end
63
+
64
+ def build_url(url)
65
+ uri = URI(url)
66
+
67
+ original_params = Rack::Utils.parse_query(uri.query)
68
+ # Strong params - throws an error if we just use except. So we have to
69
+ # permit everything first
70
+ new_params = params.permit!.except(:controller, :action, :id)
71
+
72
+ short_link_params = merge_params(original_params, new_params)
73
+
74
+ # Prevents adding a ? to the end of the url if there are no params
75
+ uri.query = short_link_params.to_query if short_link_params.present?
76
+
77
+ uri.to_s
78
+ end
79
+
80
+ def merge_params(original_params, new_params)
81
+ # if there are duplicates, new params value will take priority
82
+ original_params.merge(new_params) do |_key, original_value, new_value|
83
+ if original_value.present? && new_value.present?
84
+ new_value
85
+ elsif original_value.present?
86
+ original_value
87
+ else
88
+ new_value
89
+ end
90
+ end
91
+ end
92
+
93
+ def short_link_params
94
+ params.require(:short_link).permit(:url, :shortened_path)
95
+ end
96
+
97
+ def check_url_present
98
+ return if params[:url].present?
99
+
100
+ flash[:error] = 'URL cannot be blank'
101
+ redirect_to(darjeelink.new_short_link_path)
102
+ end
103
+
104
+ def check_url_valid
105
+ return if params[:url] =~ URI::DEFAULT_PARSER.make_regexp
106
+
107
+ flash[:error] = 'URL is not valid. Does it have https:// and are there any spaces?'
108
+ redirect_to(darjeelink.new_short_link_path)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class TrackingLinksController < Darjeelink::ApplicationController
5
+ def new
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ module ApplicationHelper
5
+ end
6
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class RebrandlyImporter
5
+ def initialize
6
+ @duplicates = []
7
+ @api = Rebrandly::Api.new
8
+ end
9
+
10
+ def import
11
+ import_rebrandly_links
12
+
13
+ Rails.logger.info("Imported #{api.link_count - duplicates.count} links from Rebrandly")
14
+ duplicate_string = duplicates.join("\n")
15
+ Rails.logger.warn("Duplicates:\n#{duplicate_string}") unless duplicates.empty?
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :api
21
+ attr_accessor :duplicates
22
+
23
+ def import_rebrandly_links
24
+ last_id = nil
25
+ imported_count = 0
26
+
27
+ while imported_count < api.link_count
28
+ api.links(last: last_id).each do |link|
29
+ begin
30
+ create_short_link(link)
31
+ last_id = link.id
32
+ rescue ActiveRecord::RecordNotUnique
33
+ duplicates << "#{link.destination}/#{link.slashtag}"
34
+ end
35
+ imported_count += 1
36
+ end
37
+ end
38
+ end
39
+
40
+ def create_short_link(link)
41
+ Darjeelink::ShortLink.create!(
42
+ url: link.destination,
43
+ shortened_path: link.slashtag,
44
+ visits: link.clicks
45
+ )
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ShortLinkImporter
5
+ def initialize(path_to_csv)
6
+ @path_to_csv = path_to_csv
7
+ @duplicates = []
8
+ end
9
+
10
+ def import
11
+ CSV.foreach(path_to_csv, headers: true).each do |row|
12
+ url, auto_generated_key, custom_key = row.fields
13
+ create_short_link(url, auto_generated_key, custom_key)
14
+ end
15
+
16
+ duplicate_string = duplicates.join("\n")
17
+ Rails.logger.warn("Duplicates:\n#{duplicate_string}") unless duplicates.empty?
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :path_to_csv
23
+ attr_accessor :duplicates
24
+
25
+ def create_short_link(url, auto_generated_key, custom_key)
26
+ shortened_path = custom_key.present? ? custom_key : auto_generated_key
27
+ ::Darjeelink::ShortLink.create!(url: url, shortened_path: shortened_path)
28
+ rescue ActiveRecord::RecordNotUnique
29
+ duplicates << "#{url}/#{custom_key || auto_generated_key}"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: 'from@example.com'
6
+ layout 'mailer'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darjeelink
4
+ class ShortLink < ApplicationRecord
5
+ after_save :generate_short_link
6
+ before_validation :strip_white_space
7
+
8
+ validates_presence_of :url
9
+
10
+ class << self
11
+ def search(query)
12
+ where(
13
+ 'shortened_path ILIKE :query', query: "%#{query}%"
14
+ ).or(
15
+ where(
16
+ 'url ILIKE :query', query: "%#{query}%"
17
+ )
18
+ )
19
+ end
20
+
21
+ def ifind(shortened_path)
22
+ where('lower(shortened_path) = ?', shortened_path.downcase).first
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def generate_short_link
29
+ return if shortened_path.present?
30
+
31
+ update!(shortened_path: auto_generate_shortened_path)
32
+ end
33
+
34
+ ALPHABET = (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + ['-', '_']).freeze
35
+
36
+ def auto_generate_shortened_path
37
+ # from http://refactormycode.com/codes/125-base-62-encoding
38
+ # with only minor modification
39
+ i = id
40
+ return ALPHABET[0] if i.zero?
41
+
42
+ generated_path = []
43
+ base = ALPHABET.length
44
+
45
+ while i.positive?
46
+ generated_path << ALPHABET[i.modulo(base)]
47
+ i /= base
48
+ end
49
+
50
+ generated_path.join('').reverse
51
+ end
52
+
53
+ def strip_white_space
54
+ url&.strip!
55
+ shortened_path&.strip!
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ <div class="row">
2
+ <div class="col-md-12">
3
+ <% if flash[:error] %>
4
+ <div class="fade in alert alert-danger alert-dismissable">
5
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
6
+ <%= flash[:error] %>
7
+ </div>
8
+ <% end %>
9
+
10
+ <%= form_with url: darjeelink.short_links_path, local: true do |f| %>
11
+ <div class="form-group">
12
+ <label for="url">Url</label>
13
+ <input type="url" class="form-control" id="long_url" name="url">
14
+ </div>
15
+ <div class="form-group">
16
+ <label for="shortened_path">Custom Path</label>
17
+ <input type="text" class="form-control" name="shortened_path">
18
+ </div>
19
+ <div>
20
+ <button type="submit">Shorten</button>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,29 @@
1
+ <div class="row">
2
+ <div class="col-md-12">
3
+ <%= link_to "Back", darjeelink.short_links_path, class: "btn btn-default" %>
4
+ </div>
5
+ </div>
6
+
7
+ <div class="row">
8
+ <div class="col-md-12">
9
+ <h1>Edit Short Link</h1>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="row">
14
+ <div class="col-md-12">
15
+ <%= form_for @short_link, url: darjeelink.short_link_path do |f| %>
16
+ <div class="form-group">
17
+ <%= f.label :url %>
18
+ <%= f.url_field :url, class: "form-control", value: @short_link.url %>
19
+ </div>
20
+ <div class="form-group">
21
+ <%= f.label :custom_path %>
22
+ <%= f.text_field :shortened_path, class: "form-control", value: @short_link.shortened_path %>
23
+ </div>
24
+ <div>
25
+ <button type="submit">Update</button>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+ </div>
@@ -0,0 +1,75 @@
1
+ <div class="row">
2
+ <div class="col-md-12">
3
+ <div class="page-header">
4
+ <h2 class="text-center header-text">
5
+ Short Links
6
+ </h2>
7
+ </div>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="row">
12
+ <div class="col-md-12 text-center">
13
+ <div class="btn-group">
14
+ <%= link_to "Tracking link generator", darjeelink.new_tracking_link_path, class: "btn btn-success" %>
15
+ <%= link_to "Create your shortened link", darjeelink.new_short_link_path, class: "btn btn-default" %>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <br/>
21
+ <div class="row text-center">
22
+ <div class="col-sm-4 col-sm-offset-4">
23
+ <%= form_tag("/short_links", method: "get") do %>
24
+ <div class="form-group">
25
+ <%= text_field_tag(:query, nil, class: 'form-control', placeholder: 'Search term') %>
26
+ </div>
27
+ <%= submit_tag("Search") %>
28
+ <% end %>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="row">
33
+ <div class="col-md-12">
34
+ <table class="table table-striped">
35
+ <thead>
36
+ <tr>
37
+ <th>URL</th>
38
+ <th>Visits</th>
39
+ <th>Edit</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ <% @short_links.each do |short_link| %>
44
+ <tr>
45
+ <td>
46
+ <a href="<%= short_link.url %>" target="_blank" id="copy-text">
47
+ <%= Darjeelink.domain + '/' + short_link.shortened_path %>
48
+ </a>
49
+ <button id="copy-btn" class="btn btn-info btn-sm" data-clipboard-text=<%= Darjeelink.domain + '/' + short_link.shortened_path %>>
50
+ <span class="glyphicon glyphicon-copy"></span>
51
+ </button>
52
+ </td>
53
+ <td>
54
+ <p>
55
+ <%= short_link.visits %>
56
+ </p>
57
+ </td>
58
+ <td>
59
+ <%= link_to "Edit", darjeelink.edit_short_link_path(short_link) %>
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ <div class="row">
66
+ <div class="col-md-12">
67
+ <%= will_paginate @short_links %>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <script>
74
+ new ClipboardJS('#copy-btn');
75
+ </script>