From eba16d518ef43a21c8ba1935f1adc5099c82cc18 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 15:06:06 +0100
Subject: [PATCH 1/9] Update README.md

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index f21c15e..af57128 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,13 @@ unnecessary features, or they didn't have all the features I wanted.
 # Screenshot
 ![Screenshot](./screenshot.png)
 
-# Planned features
-- An actual name
+# Planned features for 1.0 (in order of importance
 - Some form of authentication
 - Input validation (on client and server)
 - Deleting links using API and frontend
-- Code cleanup
 - Better deduplication
+- Code cleanup
+- An actual name
 - Official Docker Hub image
 
 # Usage

From 59b6d43aea61863a18d87e4e6572917e2da0ec7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 15:46:29 +0100
Subject: [PATCH 2/9] Added Basic auth and GZIP compression

---
 build.gradle                                 |  1 +
 src/main/java/tk/draganczuk/url/App.java     | 18 ++++++++++++++++--
 src/main/java/tk/draganczuk/url/Filters.java | 20 ++++++++++++++++++++
 src/main/resources/public/js/main.js         |  4 ++--
 4 files changed, 39 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/tk/draganczuk/url/Filters.java

diff --git a/build.gradle b/build.gradle
index 609d499..58e0dae 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,6 +23,7 @@ jar {
 
 dependencies {
 	compile "com.sparkjava:spark-core:2.8.0"
+    implementation 'com.qmetric:spark-authentication:1.4'
 }
 
 application {
diff --git a/src/main/java/tk/draganczuk/url/App.java b/src/main/java/tk/draganczuk/url/App.java
index 24bff3e..93a220a 100644
--- a/src/main/java/tk/draganczuk/url/App.java
+++ b/src/main/java/tk/draganczuk/url/App.java
@@ -1,5 +1,7 @@
 package tk.draganczuk.url;
 
+import spark.Filter;
+
 import static spark.Spark.*;
 
 public class App {
@@ -17,13 +19,25 @@ public class App {
 
 		port(Integer.parseInt(System.getProperty("port", "4567")));
 
+		// Add GZIP compression
+		after(Filters::addGZIP);
+
+		// Authenticate
+		Filter authFilter = Filters.createAuthFilter();
+		before("/index.html", authFilter);
+
 		get("/", (req, res) -> {
 			res.redirect("/index.html");
 			return "Redirect";
 		});
 
-		get("/all", Routes::getAll);
-		post("/new", Routes::addUrl);
+
+		path("/api", () -> {
+			before("/*", authFilter);
+			get("/all", Routes::getAll);
+			post("/new", Routes::addUrl);
+		});
+
 		get("/:shortUrl", Routes::goToLongUrl);
 	}
 }
diff --git a/src/main/java/tk/draganczuk/url/Filters.java b/src/main/java/tk/draganczuk/url/Filters.java
new file mode 100644
index 0000000..83ad996
--- /dev/null
+++ b/src/main/java/tk/draganczuk/url/Filters.java
@@ -0,0 +1,20 @@
+package tk.draganczuk.url;
+
+import com.qmetric.spark.authentication.AuthenticationDetails;
+import com.qmetric.spark.authentication.BasicAuthenticationFilter;
+import spark.Filter;
+import spark.Request;
+import spark.Response;
+
+public class Filters {
+    public static void addGZIP(Request request, Response response) {
+        response.header("Content-Encoding", "gzip");
+    }
+
+    public static Filter createAuthFilter() {
+        String username = System.getenv("username");
+        String password = System.getenv("password");
+
+        return new BasicAuthenticationFilter(new AuthenticationDetails(username, password));
+    }
+}
diff --git a/src/main/resources/public/js/main.js b/src/main/resources/public/js/main.js
index 0a9acbf..850ff4e 100644
--- a/src/main/resources/public/js/main.js
+++ b/src/main/resources/public/js/main.js
@@ -1,5 +1,5 @@
 const refreshData = async () => {
-    let data = await fetch("/all").then(res => res.text());
+    let data = await fetch("/api/all").then(res => res.text());
     data = data
         .split("\n")
         .filter(line => line !== "")
@@ -44,7 +44,7 @@ const submitForm = () => {
     const longUrl = form.elements["longUrl"];
     const shortUrl = form.elements["shortUrl"];
 
-    const url = `/new?long=${longUrl.value}&short=${shortUrl.value}`;
+    const url = `/api/new?long=${longUrl.value}&short=${shortUrl.value}`;
 
     fetch(url, {
         method: "POST"

From bde40b61e6aaa7eea99d92b3c7f37b84dc13577f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 15:51:00 +0100
Subject: [PATCH 3/9] Added documentation about authentication

---
 README.md          | 15 ++++++++++++---
 docker-compose.yml |  2 ++
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index af57128..f484896 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ git clone https://github.com/draganczukp/url
 ```
 ## Building from source
 Gradle 6.x.x and JDK 11 are required. Other versions are not tested
-1. Build the `.jar` file
+### 1. Build the `.jar` file
 ```
 gradle build --no-daemon
 ```
@@ -46,11 +46,18 @@ The `--no-daemon` option means that gradle should exit as soon as the build is
 finished. Without it, gradle would still be running in the background
 in order to speed up future builds.
 
-2. Run it
+### 2. Set environment variables
+```bash
+export username=<api username>
+export password=<api password>
+export file.location=<file location> # opitonal
+```
+
+### 3. Run it
 ```
 java -jar build/libs/url.jar
 ```
-3. Navigate to `http://localhost:4567` in your browser, add links as you wish.
+### 4. Navigate to `http://localhost:4567` in your browser, add links as you wish.
 
 ## Running with docker
 ### `docker run` method
@@ -67,6 +74,8 @@ docker run -p 4567:4567 -d url:1.0
 touch ./urls.csv
 docker run -p 4567:4567 \
 	-e file.location=/urls.csv \
+    -e username="username"
+    -e password="password"
 	-v ./urls.csv:/urls.csv \
 	-d url:1.0
 ```
diff --git a/docker-compose.yml b/docker-compose.yml
index 764631b..b375dde 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,5 +8,7 @@ services:
       - 4567:4567
     environment:
       - file.location=/urls.csv
+      - username=admin
+      - password=admin
     volumes:
       - ./urls.csv:/urls.csv
\ No newline at end of file

From 8cd399d2e9ce0295de6cbc1a98490be93b4dff0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 15:58:37 +0100
Subject: [PATCH 4/9] Now only lowercase letters and numbers are generated

---
 src/main/java/tk/draganczuk/url/Utils.java | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/main/java/tk/draganczuk/url/Utils.java b/src/main/java/tk/draganczuk/url/Utils.java
index 0c868c4..efa312a 100644
--- a/src/main/java/tk/draganczuk/url/Utils.java
+++ b/src/main/java/tk/draganczuk/url/Utils.java
@@ -10,14 +10,12 @@ public class Utils {
 		int rightLimit = 122; // letter 'z'
 		int targetStringLength = 10;
 
-		String generatedString = random.ints(leftLimit, rightLimit + 1)
-				.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
+		return random.ints(leftLimit, rightLimit + 1)
+				.filter(i -> (i <= 57 || i >= 97))
 				.limit(targetStringLength)
 				.collect(StringBuilder::new,
 						StringBuilder::appendCodePoint,
 						StringBuilder::append)
 				.toString();
-
-		return generatedString;
 	}
 }

From 1322569cf6b7ab036659700bc2e7f7dcb8e79c0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 16:05:09 +0100
Subject: [PATCH 5/9] Added a basic input validation for shortUrl

---
 src/main/java/tk/draganczuk/url/Routes.java | 15 +++++++++++----
 src/main/java/tk/draganczuk/url/Utils.java  |  9 +++++++++
 src/main/resources/public/index.html        |  5 +++--
 3 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/src/main/java/tk/draganczuk/url/Routes.java b/src/main/java/tk/draganczuk/url/Routes.java
index dad75b8..2ad5fd1 100644
--- a/src/main/java/tk/draganczuk/url/Routes.java
+++ b/src/main/java/tk/draganczuk/url/Routes.java
@@ -1,5 +1,6 @@
 package tk.draganczuk.url;
 
+import org.eclipse.jetty.http.HttpStatus;
 import spark.Request;
 import spark.Response;
 
@@ -29,15 +30,21 @@ public class Routes {
 			shortUrl = Utils.randomString();
 		}
 
-		return urlFile.addUrl(longUrl, shortUrl);
+		if (Utils.validate(shortUrl)) {
+			return urlFile.addUrl(longUrl, shortUrl);
+		} else {
+			res.status(HttpStatus.BAD_REQUEST_400);
+			return "shortUrl not valid ([a-z0-9]+)";
+		}
 	}
 
-	public static String goToLongUrl(Request req, Response res){
+
+	public static String goToLongUrl(Request req, Response res) {
 		String shortUrl = req.params("shortUrl");
 		var longUrlOpt = urlFile
-			.findForShortUrl(shortUrl);
+				.findForShortUrl(shortUrl);
 
-		if(longUrlOpt.isEmpty()){
+		if (longUrlOpt.isEmpty()) {
 			res.status(404);
 			return "";
 		}
diff --git a/src/main/java/tk/draganczuk/url/Utils.java b/src/main/java/tk/draganczuk/url/Utils.java
index efa312a..9292067 100644
--- a/src/main/java/tk/draganczuk/url/Utils.java
+++ b/src/main/java/tk/draganczuk/url/Utils.java
@@ -1,10 +1,14 @@
 package tk.draganczuk.url;
 
 import java.util.Random;
+import java.util.regex.Pattern;
 
 public class Utils {
 	private static final Random random = new Random(System.currentTimeMillis());
 
+	private static final String SHORT_URL_PATTERN = "[a-z0-9]+";
+	private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN);
+
 	public static String randomString() {
 		int leftLimit = 48; // numeral '0'
 		int rightLimit = 122; // letter 'z'
@@ -18,4 +22,9 @@ public class Utils {
 						StringBuilder::append)
 				.toString();
 	}
+
+	public static boolean validate(String shortUrl) {
+		return PATTERN.matcher(shortUrl)
+				.matches();
+	}
 }
diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html
index 988384d..e53d4fe 100644
--- a/src/main/resources/public/index.html
+++ b/src/main/resources/public/index.html
@@ -30,11 +30,12 @@
             <legend>Add new URL</legend>
             <div class="pure-control-group">
                 <label for="longUrl">Long URL</label>
-                <input type="text" name="longUrl" id="longUrl" placeholder="Long URL" required/>
+                <input type="url" name="longUrl" id="longUrl" placeholder="Long URL" required/>
             </div>
             <div class="pure-control-group">
                 <label for="shortUrl">Short URL (Optional)</label>
-                <input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"/>
+                <input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"
+                       pattern="[a-z0-9]+"/>
             </div>
             <div class="pure-controls">
                 <button class="pure-button pure-button-primary">Submit</button>

From 89eb5526ce43a3e608b9f38fe35b297ee3780d90 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 16:08:06 +0100
Subject: [PATCH 6/9] Added dashes and underscores to permitted characters

---
 src/main/java/tk/draganczuk/url/Utils.java | 2 +-
 src/main/resources/public/index.html       | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/main/java/tk/draganczuk/url/Utils.java b/src/main/java/tk/draganczuk/url/Utils.java
index 9292067..174a7b5 100644
--- a/src/main/java/tk/draganczuk/url/Utils.java
+++ b/src/main/java/tk/draganczuk/url/Utils.java
@@ -6,7 +6,7 @@ import java.util.regex.Pattern;
 public class Utils {
 	private static final Random random = new Random(System.currentTimeMillis());
 
-	private static final String SHORT_URL_PATTERN = "[a-z0-9]+";
+	private static final String SHORT_URL_PATTERN = "[a-z0-9_-]+";
 	private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN);
 
 	public static String randomString() {
diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html
index e53d4fe..897ebb3 100644
--- a/src/main/resources/public/index.html
+++ b/src/main/resources/public/index.html
@@ -33,9 +33,10 @@
                 <input type="url" name="longUrl" id="longUrl" placeholder="Long URL" required/>
             </div>
             <div class="pure-control-group">
-                <label for="shortUrl">Short URL (Optional)</label>
+                <label for="shortUrl">Short URL (Optional). Only letters, number dashes and underscores
+                    permitted</label>
                 <input type="text" name="shortUrl" id="shortUrl" placeholder="Short URL (optional)"
-                       pattern="[a-z0-9]+"/>
+                       pattern="[a-z0-9_-]+"/>
             </div>
             <div class="pure-controls">
                 <button class="pure-button pure-button-primary">Submit</button>

From dc171b9973d36863909433fe7b61f7e182b33f2a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 16:10:04 +0100
Subject: [PATCH 7/9] Slightly modified default docker files

---
 README.md          | 4 ++--
 docker-compose.yml | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index f484896..9f0752b 100644
--- a/README.md
+++ b/README.md
@@ -63,11 +63,11 @@ java -jar build/libs/url.jar
 ### `docker run` method
 1. Build the image
 ```
-docker build . -t url:1.0
+docker build . -t url:latest
 ```
 2. Run the image
 ```
-docker run -p 4567:4567 -d url:1.0 
+docker run -p 4567:4567 -d url:latest
 ```
 2.a Make the CSV file available to host
 ```
diff --git a/docker-compose.yml b/docker-compose.yml
index b375dde..1442403 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,6 +4,7 @@ services:
     # TODO: Publish to docker hub
     build:
       context: .
+    container_name: url
     ports:
       - 4567:4567
     environment:

From f47afab80bab26c4d87bb13962c2f8a142508890 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 16:11:14 +0100
Subject: [PATCH 8/9] Updated README.md

Removed existing features from the planned features list
---
 README.md | 2 --
 1 file changed, 2 deletions(-)

diff --git a/README.md b/README.md
index 9f0752b..6cd7a72 100644
--- a/README.md
+++ b/README.md
@@ -23,8 +23,6 @@ unnecessary features, or they didn't have all the features I wanted.
 ![Screenshot](./screenshot.png)
 
 # Planned features for 1.0 (in order of importance
-- Some form of authentication
-- Input validation (on client and server)
 - Deleting links using API and frontend
 - Better deduplication
 - Code cleanup

From 6d7b065e9898e75069aa11035f9a090ffe97f32d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= <draganczukp@gmail.com>
Date: Sun, 16 Feb 2020 16:52:54 +0100
Subject: [PATCH 9/9] Added option to delete a URL

---
 src/main/java/tk/draganczuk/url/App.java     |  1 +
 src/main/java/tk/draganczuk/url/Routes.java  | 13 +++++++
 src/main/java/tk/draganczuk/url/UrlFile.java | 36 ++++++++++++++++----
 src/main/resources/public/index.html         |  1 +
 src/main/resources/public/js/main.js         | 17 +++++++++
 5 files changed, 62 insertions(+), 6 deletions(-)

diff --git a/src/main/java/tk/draganczuk/url/App.java b/src/main/java/tk/draganczuk/url/App.java
index 93a220a..603e690 100644
--- a/src/main/java/tk/draganczuk/url/App.java
+++ b/src/main/java/tk/draganczuk/url/App.java
@@ -36,6 +36,7 @@ public class App {
 			before("/*", authFilter);
 			get("/all", Routes::getAll);
 			post("/new", Routes::addUrl);
+			delete("/:shortUrl", Routes::delete);
 		});
 
 		get("/:shortUrl", Routes::goToLongUrl);
diff --git a/src/main/java/tk/draganczuk/url/Routes.java b/src/main/java/tk/draganczuk/url/Routes.java
index 2ad5fd1..2e9b590 100644
--- a/src/main/java/tk/draganczuk/url/Routes.java
+++ b/src/main/java/tk/draganczuk/url/Routes.java
@@ -54,4 +54,17 @@ public class Routes {
 		return "";
 	}
 
+	public static String delete(Request req, Response res) {
+		String shortUrl = req.params("shortUrl");
+		var longUrlOpt = urlFile
+				.findForShortUrl(shortUrl);
+
+		if (longUrlOpt.isEmpty()) {
+			res.status(404);
+			return "";
+		}
+
+		urlFile.deleteEntry(String.format("%s,%s", shortUrl, longUrlOpt.get()));
+		return "";
+	}
 }
diff --git a/src/main/java/tk/draganczuk/url/UrlFile.java b/src/main/java/tk/draganczuk/url/UrlFile.java
index 858a5e2..f666fb2 100644
--- a/src/main/java/tk/draganczuk/url/UrlFile.java
+++ b/src/main/java/tk/draganczuk/url/UrlFile.java
@@ -3,6 +3,7 @@ package tk.draganczuk.url;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.util.List;
 import java.util.Optional;
@@ -41,18 +42,41 @@ public class UrlFile {
 
 	public Optional<String> findForShortUrl(String shortUrl){
 		try {
-			return  Files.lines(file.toPath())
-				.map(this::splitLine)
-				.filter(pair -> pair.getLeft().equals(shortUrl))
-				.map(Pair::getRight)
-				.findAny();
+			return Files.lines(file.toPath())
+					.map(this::splitLine)
+					.filter(pair -> pair.getLeft().equals(shortUrl))
+					.map(Pair::getRight)
+					.findAny();
 		} catch (IOException e) {
 			return Optional.empty();
 		}
 	}
 
-	public Pair<String, String> splitLine(String line){
+	public Pair<String, String> splitLine(String line) {
 		var split = line.split(",");
 		return new Pair<>(split[0], split[1]);
 	}
+
+	public void deleteEntry(String entry) {
+		try {
+			File tmp = File.createTempFile(file.getName(), ".tmp");
+			if (!tmp.exists()) {
+				tmp.createNewFile();
+			}
+
+			Files.lines(file.toPath())
+					.filter(line -> !line.equals(entry))
+					.forEach(line -> {
+						try {
+							Files.writeString(tmp.toPath(), line + "\n", StandardOpenOption.APPEND);
+						} catch (IOException e) {
+							e.printStackTrace();
+						}
+					});
+
+			Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
 }
diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html
index 897ebb3..15e29e4 100644
--- a/src/main/resources/public/index.html
+++ b/src/main/resources/public/index.html
@@ -50,6 +50,7 @@
         <tr>
             <td>Long URL</td>
             <td>Short url</td>
+            <td></td>
         </tr>
         </thead>
         <tbody id="url-table">
diff --git a/src/main/resources/public/js/main.js b/src/main/resources/public/js/main.js
index 850ff4e..a3c335a 100644
--- a/src/main/resources/public/js/main.js
+++ b/src/main/resources/public/js/main.js
@@ -23,9 +23,11 @@ const TR = (row) => {
     const tr = document.createElement("tr");
     const longTD = TD(A(row.long));
     const shortTD = TD(A_INT(row.short));
+    const btn = deleteButton(row.short);
 
     tr.appendChild(longTD);
     tr.appendChild(shortTD);
+    tr.appendChild(btn);
 
     return tr;
 };
@@ -33,6 +35,21 @@ const TR = (row) => {
 const A = (s) => `<a href='${s}'>${s}</a>`;
 const A_INT = (s) => `<a href='/${s}'>${window.location.host}/${s}</a>`;
 
+const deleteButton = (shortUrl) => {
+    const btn = document.createElement("button");
+
+    btn.innerHTML = "&times;";
+
+    btn.onclick = e => {
+        e.preventDefault();
+        fetch(`/api/${shortUrl}`, {
+            method: "DELETE"
+        }).then(_ => refreshData());
+    };
+
+    return btn;
+};
+
 const TD = (s) => {
     const td = document.createElement("td");
     td.innerHTML = s;