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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +82 -0
- data/Rakefile +24 -0
- data/app/assets/config/darjeelink_manifest.js +2 -0
- data/app/assets/javascripts/darjeelink/application.js +15 -0
- data/app/assets/javascripts/darjeelink/sparkleh.js +248 -0
- data/app/assets/javascripts/darjeelink/tracking_link_generator.js +36 -0
- data/app/assets/stylesheets/darjeelink/application.css +15 -0
- data/app/controllers/darjeelink/application_controller.rb +31 -0
- data/app/controllers/darjeelink/sessions_controller.rb +31 -0
- data/app/controllers/darjeelink/short_links_controller.rb +111 -0
- data/app/controllers/darjeelink/tracking_links_controller.rb +8 -0
- data/app/helpers/darjeelink/application_helper.rb +6 -0
- data/app/importers/darjeelink/rebrandly_importer.rb +48 -0
- data/app/importers/darjeelink/short_link_importer.rb +32 -0
- data/app/jobs/darjeelink/application_job.rb +6 -0
- data/app/mailers/darjeelink/application_mailer.rb +8 -0
- data/app/models/darjeelink/application_record.rb +7 -0
- data/app/models/darjeelink/short_link.rb +58 -0
- data/app/views/darjeelink/short_links/_short_link_form.erb +24 -0
- data/app/views/darjeelink/short_links/edit.html.erb +29 -0
- data/app/views/darjeelink/short_links/index.html.erb +75 -0
- data/app/views/darjeelink/short_links/new.html.erb +12 -0
- data/app/views/darjeelink/tracking_links/new.erb +49 -0
- data/app/views/layouts/darjeelink/application.html.erb +39 -0
- data/config/initializers/darjeelink.rb +31 -0
- data/config/initializers/omniauth.rb +10 -0
- data/config/initializers/rebrandly.rb +5 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20190103122918_create_portkey_short_links.rb +16 -0
- data/db/migrate/20190304094710_case_insensitive_index_short_link_shortened_path.rb +14 -0
- data/db/migrate/20200505220716_rename_portkey_short_links_to_darjeelink_short_links.rb +7 -0
- data/db/migrate/20200505221026_rename_portkey_short_link_index.rb +15 -0
- data/lib/darjeelink.rb +30 -0
- data/lib/darjeelink/configuration.rb +19 -0
- data/lib/darjeelink/engine.rb +23 -0
- data/lib/darjeelink/version.rb +5 -0
- data/lib/tasks/darjeelink.rake +6 -0
- 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,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,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">×</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>
|