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