corpshort 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ <section>
2
+ <h2>Edit: <%= @link.name %></h2>
3
+
4
+ <form action='<%= update_path(@link) %>' method='POST'>
5
+ <input type='hidden' name='_method' value='PUT'>
6
+ <div class='form-row'>
7
+ <label for='edit_new_name'>Name</label>
8
+ <input type='text' id='edit_new_name' name='new_name' placeholder='short-name' value="<%= @link.name %>">
9
+ </div>
10
+ <div class='form-row'>
11
+ <label for='edit_url'>URL</label>
12
+ <input type='text' id='edit_url' name='url' placeholder='https://...' value="<%= @link.url %>">
13
+ </div>
14
+ <input type='submit' value='Save'>
15
+ </form>
16
+
17
+ <p>Entering a new name will create an another link for the URL. To rename completely, manually delete the old link; Be careful with renaming existing links.</p>
18
+
19
+ <hr>
20
+
21
+ <form action='<%= update_path(@link) %>' method='POST'>
22
+ <input type='hidden' name='_method' value='DELETE'>
23
+ <p><input type='submit' onclick="return confirm('Are you sure?')" class='btn-danger' value='Delete'></p>
24
+ </form>
25
+ </section>
26
+
@@ -0,0 +1,24 @@
1
+ <section class='new-link'>
2
+ <form action='/+/links' method='POST'>
3
+ <p><input type='text' name='url' placeholder='https://...' class='new-link-url'></p>
4
+ <p class='new-link-down'>⬇︎</p>
5
+ <p><input type='text' name='name' placeholder='Short name' class='new-link-name'></p>
6
+ <p><input type='submit' value='Shorten'></p>
7
+ </form>
8
+ </section>
9
+
10
+ <div class='infos'>
11
+ <% if notice_message %>
12
+ <section>
13
+ <h3>Notice</h3>
14
+ <%== notice_message %>
15
+ </section>
16
+ <% end %>
17
+ <section>
18
+ <h3>Usage</h3>
19
+ <form action='/+' method='GET'>
20
+ <p>Edit a link: <input type='text' name='show' placeholder='name...'> <input type='submit' value='Go'></p>
21
+ </form>
22
+ <p>Or add + (plus) sign after the URL like: <%= base_url %>/something → /something+</p>
23
+ </section>
24
+ </div>
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset='utf-8'>
5
+ <meta name='viewport' content='width=device-width, minimum-scale=1'>
6
+ <title>Corpshort</title>
7
+ <link rel='stylesheet' href='/style.css' type="text/css">
8
+ </head>
9
+
10
+ <body>
11
+ <div class="container">
12
+ <header class="header">
13
+ <h1 class='logo'><a href="/">Corpshort</a></h1>
14
+ <nav>
15
+ <a href="/">Create</a>
16
+ <a href="/+/links">Recent</a>
17
+ </nav>
18
+ </header>
19
+ <% notice ||= session.delete(:notice); error ||= session.delete(:error) %>
20
+ <% if notice %>
21
+ <div class="notice"><%= notice %></div>
22
+ <% end %>
23
+
24
+ <% if error %>
25
+ <div class="error"><%= error %></div>
26
+ <% end %>
27
+
28
+ <div class="box">
29
+ <%== yield %>
30
+ </div>
31
+
32
+ <footer>
33
+ <div class="credit">
34
+ Powered by <a href="https://github.com/sorah/corpshort">sorah/corpshort</a>
35
+ </div>
36
+ </footer>
37
+ </div>
38
+ </body>
39
+ </html>
40
+
@@ -0,0 +1,11 @@
1
+ <h1><%= @title %></h1>
2
+
3
+ <ul>
4
+ <% @links.each do |name| %>
5
+ <li><a href='/<%= name %>+'><%= name %></li>
6
+ <% end %>
7
+ </ul>
8
+
9
+ <% if @next_token %>
10
+ <a href="<%= request.path %>?token=<%= URI.encode_www_form_component(@next_token) %>">More...</a>
11
+ <% end %>
@@ -0,0 +1,91 @@
1
+ <section class='link'>
2
+ <header>
3
+ <h2><a href="<%= short_link_url(@link) %>"><%= short_base_url.gsub(%r{\A.+://}, '') %>/<%= @link.name %></a></h2>
4
+ <a href='#' class='btn-link copy_btn'><input class='copy_btn_input' tabindex='-1' value="<%= short_link_url(@link) %>"><span class='copy_btn_text'>Copy</span></a>
5
+ <a href="<%= edit_path(@link) %>" class='btn-link'>Edit</a>
6
+ </header>
7
+
8
+ <p><a class='link-url' href="<%= @link.url %>"><%= @link.url %></a></p>
9
+
10
+ <div class='infos'>
11
+ <section>
12
+ <h3>Embed</h3>
13
+ <p>PDF is recommended for Keynote</p>
14
+ <div class='barcode-style'>
15
+ <div class='barcode-style-diagram barcode-style-diagram-cu-horizontal'>
16
+ <div class='barcode-style-diagram-code'></div>
17
+ <div class='barcode-style-diagram-url'></div>
18
+ </div>
19
+ <div class='barcode-style-links'>
20
+ <h4>Code + URL (Horizontal)</h4>
21
+ <ul>
22
+ <li><a href="<%= barcode_path(@link, 'horizontal', 'pdf', flex: true) %>">PDF</a></li>
23
+ <li><a href="<%= barcode_path(@link, 'horizontal', 'pdf') %>">PDF (Short)</a></li>
24
+ </ul>
25
+ </div>
26
+ </div>
27
+ <div class='barcode-style'>
28
+ <div class='barcode-style-diagram barcode-style-diagram-cu-vertical'>
29
+ <div class='barcode-style-diagram-code'></div>
30
+ <div class='barcode-style-diagram-url'></div>
31
+ </div>
32
+ <div class='barcode-style-links'>
33
+ <h4>Code + URL (Vertical)</h4>
34
+ <ul>
35
+ <li><a href="<%= barcode_path(@link, 'vertical', 'pdf', flex: true) %>">PDF</a></li>
36
+ <li><a href="<%= barcode_path(@link, 'vertical', 'pdf') %>">PDF (Short)</a></li>
37
+ </ul>
38
+ </div>
39
+ </div>
40
+ <div class='barcode-style'>
41
+ <div class='barcode-style-diagram barcode-style-diagram-codeonly'>
42
+ <div class='barcode-style-diagram-code'></div>
43
+ </div>
44
+ <div class='barcode-style-links'>
45
+ <h4>Code Only</h4>
46
+ <small>Code + URL is recommended</small>
47
+ <ul>
48
+ <li><a href="<%= barcode_path(@link, 'small', 'pdf') %>">PDF</a></li>
49
+ <li><a href="<%= barcode_path(@link, 'small', 'svg') %>">SVG</a></li>
50
+ <li><a href="<%= barcode_path(@link, 'small', 'png') %>">PNG</a></li>
51
+ </ul>
52
+ </div>
53
+ </div>
54
+ </section>
55
+ <section>
56
+ <h3>Metadata</h3>
57
+ <ul>
58
+ <li>Updated at: <%= @link.updated_at.utc.iso8601 %></li>
59
+ </ul>
60
+ </section>
61
+ <section>
62
+ <h3>Actions</h3>
63
+ <nav>
64
+ <p><a href="<%= urls_path(@link.url) %>">List all links for this URL</a></p>
65
+ </nav>
66
+ </section>
67
+
68
+ <script type='text/javascript'>
69
+ "use strict";
70
+ document.addEventListener("DOMContentLoaded", function() {
71
+ const copySupported = document.queryCommandSupported("copy");
72
+ document.querySelectorAll(".copy_btn").forEach(function(elem) {
73
+ if (!copySupported) {
74
+ elem.className += ' hidden';
75
+ return;
76
+ }
77
+ elem.addEventListener("click", function(e) {
78
+ const value = e.currentTarget.querySelector(".copy_btn_input");
79
+ value.focus();
80
+ value.select();
81
+ document.execCommand("copy");
82
+ e.currentTarget.querySelector(".copy_btn_text").innerHTML = "Copied!";
83
+ e.preventDefault();
84
+ });
85
+ elem.addEventListener("mouseleave", function(e) {
86
+ e.currentTarget.querySelector(".copy_btn_text").innerHTML = "Copy";
87
+ });
88
+ });
89
+ });
90
+ </script>
91
+ </section>
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "corpshort"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,40 @@
1
+ require 'bundler/setup'
2
+ require 'securerandom'
3
+
4
+ require 'corpshort'
5
+
6
+ if ENV['RACK_ENV'] == 'production'
7
+ raise 'Set $SECRET_KEY_BASE' unless ENV['SECRET_KEY_BASE']
8
+ end
9
+
10
+ config = {
11
+ base_url: ENV['CORPSHORT_BASE_URL'],
12
+ short_base_url: ENV['CORPSHORT_SHORT_BASE_URL'],
13
+ }
14
+
15
+ case ENV.fetch('CORPSHORT_BACKEND', 'redis')
16
+ when 'redis'
17
+ require 'corpshort/backends/redis'
18
+ config[:backend] = Corpshort::Backends::Redis.new(
19
+ redis: ENV.key?('REDIS_URL') ? lambda { Redis.new(url: ENV['REDIS_URL']) } : Redis.method(:current),
20
+ prefix: ENV.fetch('CORPSHORT_REDIS_PREFIX', 'corpshort:'),
21
+ )
22
+ when 'dynamodb'
23
+ require 'corpshort/backends/dynamodb'
24
+ config[:backend] = Corpshort::Backends::Dynamodb.new(
25
+ region: ENV.fetch('CORPSHORT_DYNAMODB_REGION'),
26
+ table: ENV.fetch('CORPSHORT_DYNAMODB_TABLE'),
27
+ )
28
+ else
29
+ raise ArgumentError, "Unsupported $CORPSHORT_BACKEND"
30
+ end
31
+
32
+ use(
33
+ Rack::Session::Cookie,
34
+ key: 'corpshortsess',
35
+ expire_after: 86400,
36
+ secure: ENV.fetch('CORPSHORT_SECURE_SESSION', ENV['RACK_ENV'] == 'production' ? '1' : nil) == '1',
37
+ secret: ENV.fetch('SECRET_KEY_BASE', SecureRandom.base64(256)),
38
+ )
39
+
40
+ run Corpshort.app(config)
@@ -0,0 +1,39 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "corpshort/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "corpshort"
8
+ spec.version = Corpshort::VERSION
9
+ spec.authors = ["Sorah Fukumori"]
10
+ spec.email = ["sorah@cookpad.com"]
11
+
12
+ spec.summary = %q{"go/" like private link shortener for internal purpose}
13
+ spec.homepage = "https://github.com/sorah/corpshort"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "sinatra"
24
+ spec.add_dependency "rack-protection"
25
+
26
+ spec.add_dependency "erubi"
27
+
28
+ spec.add_dependency "redis"
29
+ spec.add_dependency "aws-sdk-dynamodb"
30
+
31
+ spec.add_dependency "rqrcode"
32
+ spec.add_dependency "prawn"
33
+ spec.add_dependency "prawn-qrcode"
34
+
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency "rake"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "rack-test"
39
+ end
@@ -0,0 +1,7 @@
1
+ require "corpshort/version"
2
+ require "corpshort/link"
3
+ require "corpshort/app"
4
+
5
+ module Corpshort
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,349 @@
1
+ require 'rqrcode'
2
+ require 'prawn'
3
+ require 'prawn/qrcode'
4
+
5
+ require 'json'
6
+ require 'erubi'
7
+ require 'sinatra/base'
8
+ require 'rack/protection'
9
+
10
+ require 'corpshort/link'
11
+ require 'corpshort/vertical_pdf'
12
+ require 'corpshort/horizontal_pdf'
13
+
14
+ require 'uri'
15
+
16
+ module Corpshort
17
+ def self.app(*args)
18
+ App.rack(*args)
19
+ end
20
+
21
+ class App < Sinatra::Base
22
+ CONTEXT_RACK_ENV_NAME = 'corpshort.ctx'
23
+
24
+ def self.initialize_context(config)
25
+ {
26
+ config: config,
27
+ }
28
+ end
29
+
30
+ def self.rack(config={})
31
+ klass = App
32
+
33
+ test = config[:test]
34
+ session = {}
35
+ context = initialize_context(config)
36
+ lambda { |env|
37
+ env['rack.session'] = session if test # FIXME:
38
+ env[CONTEXT_RACK_ENV_NAME] = context
39
+ klass.call(env)
40
+ }
41
+ end
42
+
43
+ configure do
44
+ enable :logging
45
+ end
46
+
47
+ set :root, File.expand_path(File.join(__dir__, '..', '..', 'app'))
48
+ set :erb, :escape_html => true
49
+
50
+ use Rack::Protection::FrameOptions
51
+ use Rack::Protection::HttpOrigin
52
+ use Rack::Protection::IPSpoofing
53
+ use Rack::Protection::JsonCsrf
54
+ use Rack::Protection::PathTraversal
55
+ use Rack::Protection::RemoteToken, only_if: -> (env) { ! env['PATH_INFO'].start_with?('/+api') }
56
+ use Rack::Protection::SessionHijacking
57
+ use Rack::Protection::XSSHeader
58
+
59
+ use Rack::MethodOverride
60
+
61
+ helpers do
62
+ include Prawn::Measurements
63
+
64
+ def context
65
+ request.env[CONTEXT_RACK_ENV_NAME]
66
+ end
67
+
68
+ def conf
69
+ context.fetch(:config)
70
+ end
71
+
72
+ def notice_message
73
+ conf[:notice_message]
74
+ end
75
+
76
+ def base_url
77
+ conf[:base_url] || request.base_url
78
+ end
79
+
80
+ def short_base_url
81
+ conf[:short_base_url] || base_url
82
+ end
83
+
84
+
85
+ def backend
86
+ @backend ||= conf.fetch(:backend)
87
+ end
88
+
89
+ def link_name(name = params[:name])
90
+ name.tr('_', '-')
91
+ end
92
+
93
+ def short_link_url(link, **kwargs)
94
+ link_url(link, base_url: short_base_url, **kwargs)
95
+ end
96
+
97
+ def link_url(link, protocol: true, base_url: self.base_url())
98
+ name = link.is_a?(String) ? link_name(link) : link.name
99
+ "#{base_url}/#{name}".yield_self do |url|
100
+ if protocol
101
+ url
102
+ else
103
+ url.gsub(/\Ahttps?:\/\//, '')
104
+ end
105
+ end
106
+ end
107
+
108
+ def edit_path(link)
109
+ "/+/links/#{URI.encode_www_form_component(link.name)}/edit"
110
+ end
111
+
112
+ def update_path(link)
113
+ "/+/links/#{URI.encode_www_form_component(link.name)}"
114
+ end
115
+
116
+ def urls_path(url)
117
+ "/+/urls/#{url}"
118
+ end
119
+
120
+ def barcode_path(link, kind, ext, flex: nil)
121
+ "/+/links/#{URI.encode_www_form_component(link.name)}/#{kind}.#{ext}#{flex.nil? ? nil : "?flex=#{flex}"}"
122
+ end
123
+ end
124
+
125
+ get '/' do
126
+ erb :index
127
+ end
128
+
129
+ ## Pages
130
+
131
+ get '/+' do
132
+ if params[:show]
133
+ redirect "/+/links/#{link_name(params[:show])}"
134
+ end
135
+ halt 404
136
+ end
137
+
138
+ post '/+/links' do
139
+ unless params[:name] && params[:url]
140
+ session[:error] = "Name and URL are required"
141
+ redirect '/'
142
+ end
143
+
144
+ begin
145
+ link = Link.new({name: link_name, url: params[:url]})
146
+ link.save!(backend, create_only: true)
147
+ rescue Corpshort::Link::ValidationError, Corpshort::Backends::Base::ConflictError
148
+ session[:error] = $!.message
149
+ redirect '/'
150
+ end
151
+
152
+ redirect "/#{link.name}+"
153
+ end
154
+
155
+ get '/+/links' do
156
+ @links, @next_token = backend.list_links(token: params[:token])
157
+ @title = "Recent links"
158
+ erb :list
159
+ end
160
+
161
+ get '/+/links/*name/small.svg' do
162
+ @link = backend.get_link(params[:name])
163
+ halt 404, "not found" unless @link
164
+
165
+ content_type :svg
166
+ RQRCode::QRCode.new(link_url(@link), level: :m).as_svg(module_size: 6)
167
+ end
168
+ get '/+/links/*name/small.png' do
169
+ @link = backend.get_link(params[:name])
170
+
171
+ halt 404, "not found" unless @link
172
+ content_type :png
173
+ RQRCode::QRCode.new(link_url(@link), level: :m).as_png(size: 120).to_datastream.to_s
174
+ end
175
+ get '/+/links/*name/small.pdf' do
176
+ @link = backend.get_link(params[:name])
177
+ halt 404, "not found" unless @link
178
+
179
+ content_type :pdf
180
+ Prawn::Document.new(page_size: [cm2pt(2), cm2pt(2)], margin: 0) do |pdf|
181
+ pdf.fill_color 'FFFFFF'
182
+ pdf.fill { pdf.rounded_rectangle [cm2pt(2), cm2pt(2)], cm2pt(2), cm2pt(2), 10 }
183
+ pdf.print_qr_code(link_url(@link), level: :m, extent: cm2pt(2), stroke: false)
184
+ end.render
185
+ end
186
+
187
+ get '/+/links/*name/vertical.pdf' do
188
+ @link = backend.get_link(params[:name])
189
+ halt 404, "not found" unless @link
190
+
191
+ content_type :pdf
192
+
193
+ VerticalPdf.new(
194
+ url: link_url(@link),
195
+ base_url: short_base_url.sub(%r{\A.+://}, ''),
196
+ name: @link.name,
197
+ flex: params[:flex],
198
+ ).document.render
199
+ end
200
+
201
+ get '/+/links/*name/horizontal.pdf' do
202
+ @link = backend.get_link(params[:name])
203
+ halt 404, "not found" unless @link
204
+
205
+ content_type :pdf
206
+ HorizontalPdf.new(
207
+ url: link_url(@link),
208
+ base_url: short_base_url.sub(%r{\A.+://}, ''),
209
+ name: @link.name,
210
+ flex: params[:flex],
211
+ ).document.render
212
+ end
213
+
214
+
215
+ get '/+/links/*name/edit' do
216
+ @link = backend.get_link(params[:name])
217
+ if @link
218
+ erb :edit
219
+ else
220
+ halt 404, "not found"
221
+ end
222
+ end
223
+
224
+ get '/+/links/*name' do
225
+ redirect "/#{params[:name]}+"
226
+ end
227
+
228
+ put '/+/links/*name' do
229
+ @link = backend.get_link(params[:name])
230
+ halt 404, "not found" unless @link
231
+
232
+ @link.url = params[:url] if params[:url]
233
+
234
+ rename = params[:new_name] && @link.name != params[:new_name]
235
+ if rename
236
+ new_name = link_name(params[:new_name])
237
+ @link = Link.new(name: new_name, url: @link.url)
238
+ # Link.validate_name(new_name)
239
+ # backend.rename_link(@link, new_name)
240
+ end
241
+
242
+ begin
243
+ @link.save!(backend, create_only: rename)
244
+ rescue Corpshort::Link::ValidationError, Corpshort::Backends::Base::ConflictError
245
+ session[:error] = $!.message
246
+ redirect "/+/links/#{@link.name}/edit"
247
+ end
248
+
249
+ redirect "/#{@link.name}+"
250
+ end
251
+
252
+ delete '/+/links/*name' do
253
+ backend.delete_link(params[:name])
254
+ redirect "/"
255
+ end
256
+
257
+ get '/+/urls/*url' do
258
+ url = env['REQUEST_URI'][8..-1]
259
+ @links, @next_token = backend.list_links_by_url(url), nil
260
+ @title = "Links for URL #{url}"
261
+ erb :list
262
+ end
263
+
264
+ ## API
265
+
266
+ get '/+api/links' do
267
+ content_type :json
268
+ links, next_token = backend.list_links(token: params[:token])
269
+ {links: links, next_token: next_token}.to_json
270
+ end
271
+
272
+ post '/+api/links' do
273
+ content_type :json
274
+
275
+ unless params[:name] && params[:url]
276
+ halt 400, '{"error": "missing_params", "error_message": "name and url are required"}'
277
+ end
278
+
279
+ begin
280
+ link = Link.new({name: link_name, url: params[:url]})
281
+ link.save!(backend, create_only: true)
282
+ rescue Corpshort::Link::ValidationError => e
283
+ halt(400, {error: :validation_error, error_message: e.message}.to_json)
284
+ rescue Corpshort::Backends::Base::ConflictError
285
+ halt(409, {error: :conflict, error_message: e.message}.to_json)
286
+ end
287
+
288
+ link.to_json
289
+ end
290
+
291
+ get '/+api/links/*name' do
292
+ content_type :json
293
+ link = backend.get_link(link_name)
294
+ halt 404, '{"error": "not_found"}' unless link
295
+ link.to_json
296
+ end
297
+
298
+ put '/+api/links/*name' do
299
+ content_type :json
300
+ link = backend.get_link(link_name)
301
+ halt 404, '{"error": "not_found"}' unless link
302
+ link.url = params[:url] if params[:url]
303
+
304
+ begin
305
+ link.save!(backend)
306
+ rescue Corpshort::Link::ValidationError => e
307
+ halt(400, {error: :validation_error, error_message: e.message}.to_json)
308
+ rescue Corpshort::Backends::Base::ConflictError
309
+ halt(409, {error: :conflict, error_message: e.message}.to_json)
310
+ end
311
+ link.to_json
312
+ end
313
+
314
+ delete '/+api/links/*name' do
315
+ backend.delete_link(link_name)
316
+ status 202
317
+ ""
318
+ end
319
+
320
+ get '/+api/urls/*url' do
321
+ content_type :json
322
+ url = env['REQUEST_URI'][8..-1]
323
+ links, next_token = backend.list_links_by_url(url), nil
324
+ {links: links, next_token: next_token}.to_json
325
+ end
326
+
327
+ ## Shortlink
328
+
329
+ get '/*name' do
330
+ name = params[:name]
331
+ show = name.end_with?('+')
332
+ if show
333
+ name = name[0..-2]
334
+ end
335
+
336
+ @link = backend.get_link(link_name(name))
337
+
338
+ unless @link
339
+ halt 404, 'not found'
340
+ end
341
+
342
+ if show
343
+ erb :show
344
+ else
345
+ redirect @link.url
346
+ end
347
+ end
348
+ end
349
+ end