diff --git a/app/controllers/link.cr b/app/controllers/link.cr index 26672ac..24b1ee8 100644 --- a/app/controllers/link.cr +++ b/app/controllers/link.cr @@ -8,12 +8,14 @@ module App::Controllers::Link include App::Lib def call(env) + user = env.get("user").as(User) body = parse_body(env, ["url"]) link = Link.new link.id = UUID.v4.to_s link.url = body["url"].to_s link.slug = Random::Secure.urlsafe_base64(4) + link.user = user changeset = Database.insert(link) if !changeset.valid? @@ -48,17 +50,17 @@ module App::Controllers::Link end end - class Read < App::Lib::BaseController + class All < App::Lib::BaseController include App::Models include App::Lib def call(env) - id = env.params.url["id"] + user = env.get("user").as(User) - link = Database.get(Link, id) - raise App::NotFoundException.new(env) if !link + query = Database::Query.where(user_id: user.id.as(String)) + links = Database.all(Link, query) - response = {"data" => App::Serializers::Link.new(link)} + response = {"data" => links.map { |link| App::Serializers::Link.new(link) }} response.to_json end end @@ -68,12 +70,17 @@ module App::Controllers::Link include App::Lib def call(env) + user = env.get("user").as(User) id = env.params.url["id"] body = parse_body(env, ["url"]) link = Database.get(Link, id) raise App::NotFoundException.new(env) if !link + if link.user_id != user.id + raise App::ForbiddenException.new(env) + end + link.url = body["url"].to_s link.click_counter = 0 @@ -92,11 +99,16 @@ module App::Controllers::Link include App::Lib def call(env) + user = env.get("user").as(User) id = env.params.url["id"] link = Database.get(Link, id) raise App::NotFoundException.new(env) if !link + if link.user_id != user.id + raise App::ForbiddenException.new(env) + end + result = Database.raw_exec("DELETE FROM links WHERE id = (?)", link.id) # tempfix: Database.delete does not work if result.rows_affected == 0 raise App::UnprocessableEntityException.new(env, { "id" => ["Row delete failed"] }) diff --git a/app/models/link.cr b/app/models/link.cr index 875a722..a2d3725 100644 --- a/app/models/link.cr +++ b/app/models/link.cr @@ -1,6 +1,8 @@ require "sqlite3" require "crecto" +require "./user.cr" + module App::Models class Link < Crecto::Model schema :links do @@ -8,9 +10,13 @@ module App::Models field :slug, String field :url, String field :click_counter, Int64, default: 0 + + belongs_to :user, User end unique_constraint :slug + validate_required [:slug, :url] + validate_format :url, /\A(?:https?:\/\/)?(?:[\w-]+\.)+[\w-]+(?:\/\S*)?/ end end diff --git a/app/models/user.cr b/app/models/user.cr new file mode 100644 index 0000000..8d24202 --- /dev/null +++ b/app/models/user.cr @@ -0,0 +1,16 @@ +require "sqlite3" +require "crecto" + +module App::Models + class User < Crecto::Model + schema :users do + field :id, String, primary_key: true + field :name, String + field :api_key, String + end + + validate_required [:name, :api_key] + + has_many :links, Link, dependent: :destroy + end +end diff --git a/bruno/environments/local.bru b/bruno/environments/local.bru index 80a5d50..26834d2 100644 --- a/bruno/environments/local.bru +++ b/bruno/environments/local.bru @@ -1,3 +1,4 @@ vars { baseUrl: http://localhost:4000 + apiKey: xYBDV_vGC3_pcGss3Z2lGA } diff --git a/db/migrations/20240512214223_create_links.sql b/db/migrations/20240512214223_create_links.sql index bc1d36a..d7e90c7 100644 --- a/db/migrations/20240512214223_create_links.sql +++ b/db/migrations/20240512214223_create_links.sql @@ -2,11 +2,14 @@ -- SQL in section 'Up' is executed when this migration is applied CREATE TABLE links ( id TEXT PRIMARY KEY NOT NULL, - slug VARCHAR(100) UNIQUE NOT NULL, + user_id TEXT NOT NULL, + slug VARCHAR(4) UNIQUE NOT NULL, url TEXT NOT NULL, click_counter INTEGER NOT NULL DEFAULT 0, created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL + updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- +micrate Down diff --git a/db/migrations/20240513115731_create_users.sql b/db/migrations/20240513115731_create_users.sql new file mode 100644 index 0000000..fc6d02a --- /dev/null +++ b/db/migrations/20240513115731_create_users.sql @@ -0,0 +1,13 @@ +-- +micrate Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE users ( + id TEXT PRIMARY KEY NOT NULL, + name VARCHAR(100) NOT NULL, + api_key VARCHAR(64) UNIQUE NOT NULL, + created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +-- +micrate Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE users; diff --git a/db/migrations/20240513130054_add_api_key_index_to_users.sql b/db/migrations/20240513130054_add_api_key_index_to_users.sql new file mode 100644 index 0000000..223f1cb --- /dev/null +++ b/db/migrations/20240513130054_add_api_key_index_to_users.sql @@ -0,0 +1,7 @@ +-- +micrate Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE INDEX IF NOT EXISTS index_users_api_key ON users (api_key); + +-- +micrate Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP INDEX IF EXISTS index_users_api_key; diff --git a/url-shortener.cr b/url-shortener.cr index d1a7cb6..f57727b 100644 --- a/url-shortener.cr +++ b/url-shortener.cr @@ -4,11 +4,15 @@ require "./app/config/*" require "./app/lib/*" require "./app/models/*" require "./app/serializers/*" +require "./app/middlewares/*" require "./app/routes" -error 500 { |env| { "error" => "Internal Server Error" }.to_json } -error 401 { |env| { "error" => "Unauthorized" }.to_json } -error 404 { |env| { "error" => "Not Found" }.to_json } +add_context_storage_type(App::Models::User) +add_handler(App::Middlewares::Auth.new) + +error 500 { |env| {"error" => "Internal Server Error" }.to_json} +error 401 { |env| {"error" => "Unauthorized" }.to_json} +error 404 { |env| {"error" => "Not Found" }.to_json} Kemal.run