darjeelink 0.11.5

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