This commit is contained in:
Jesper 2022-10-19 22:13:51 +02:00
parent e4d1ddb69b
commit 01ae499eba
12 changed files with 611 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Config file
config.toml

233
assets/style.css Normal file
View File

@ -0,0 +1,233 @@
body {
background-color: #202224;
/* color: #dbdbdb; */
color: #FFF ;
font-family: 'Open Sans', sans-serif;
margin-bottom: 200px ;
/* max-width:650px; */
line-height:1.6;
font-size:18px;
margin: 0;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
.active {
background-color: #292C2E;
}
li
{
float: left;
/*display: inline;*/
}
li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
/* Change the link color to #111 (black) on hover */
li a:hover {
background-color: #111;
}
.main {
margin: auto ;
text-align: center;
}
.main svg {
vertical-align: -.3em;
margin-right: .3rem;
-webkit-filter: invert(100%);
filter: invert(100%)
}
.main svg:hover {
filter: invert(0%)
}
p img, li img, h1 img, h2 img, h3 img, h4 img {
vertical-align: middle ;
max-width: 1em;
max-height: 1em;
border: none ;
display: inline ;
}
img {
max-width: 90% ;
margin: auto ;
display: block ;
border: solid 5px beige ;
}
.titleimg {
border: none ;
height: 150px ;
}
h1 {
text-align: center ;
color: lightgreen ;
}
header h1 {
font-size: 40px ;
}
h2 {
text-align: center ;
color: deeppink ;
font-variant: small-caps;
font-size: 24pt ;
border-bottom: dashed #ddd 1px ;
max-width: 500px ;
margin: 1em auto ;
}
footer {
text-align: center ;
font-variant: small-caps ;
clear: both ;
padding: 2em 0 ;
}
footer li {
display: inline-block ;
padding: 0 .5em ;
font-size: x-large ;
}
footer li:hover {
background: lightblue ;
}
footer { font-size: large ; }
h1,h2,h3{
font-family: Arial;
}
a{
color:royalblue;
text-decoration:none;
}
a:hover{color:lightblue;}
code {
color: lime ;
border-radius: 5px ;
}
aside {
border: solid 1px black ;
border-radius: 20px ;
padding: 0 1em 0 1em ;
font-size: small ;
}
aside p {
color: gray ;
}
aside code {
color: green ;
}
/* .callout here is refencing any aside given the class name callout
* An example being: <aside class="callout"> */
aside.callout {
border: solid 1px orange;
}
.cnp {
width: 100% ;
}
.cryptocontainer {
display: flex ;
flex-wrap: wrap ;
justify-content: center ;
display: table;
}
/* This "@media" block defines rules that will only be applied when the minimum
* width of the screen is 55em or greater. In essence, they are settings that
* only apply on normal weide screens, but *not* phones and low res monitors.
* Since we have more room on widescreens, we change a couple things. */
@media (min-width: 55em) {
aside {
margin: 0 30px 0 30px;
}
.resright, .disappear {
display: block ;
float: right;
padding: 20px ;
clear: both ;
max-height: 400px ;
max-width: 300px ;
}
header { max-width: 900px ; margin: auto;}
main { max-width: 850px ; }
}
.ll { font-size: large ; line-height: 1.3em ; max-width: 600px; margin: auto ; }
/* These settings are for the cryptocurrency donation QR codes and info on the
* main page. */
.qr { max-width: 150px ; padding: 10px; border: none; }
.cryptocontainer {
display: flex ;
flex-wrap: wrap ;
justify-content: center ;
}
.cryptoinfo {
max-width: 350px ;
text-align: center ;
padding-left: 10px ;
padding-right: 10px ;
}
.cryptoinfo code,.crypto {
font-size: small ;
overflow-wrap: break-word ;
}
/* The "Next Article" Button changes color and also has a 👉 automatically
* added to its front. */
@-webkit-keyframes next {
0% {color: yellow ;}
100% {color: lightblue}
}
.next a {
color: inherit ;
}
.next {
color: red ;
-webkit-animation:next 1s infinite alternate ;
font-size: xx-large ;
text-align: center ;
margin: auto ;
display: block ;
font-weight: bold ;
padding: 1em ;
}
.next:before {
content: "👉" ;
}

4
config.toml.example Normal file
View File

@ -0,0 +1,4 @@
MYSQL_DB = "forkort"
MYSQL_USER = "forkort"
MYSQL_PASS = "Passw0rd"
MYSQL_HOST = "127.0.0.1:3306"

BIN
forkort.dk Executable file

Binary file not shown.

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module forkort.dk
go 1.19
require (
github.com/BurntSushi/toml v1.2.0
github.com/go-sql-driver/mysql v1.6.0
github.com/gorilla/mux v1.8.0
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=

231
main.go Normal file
View File

@ -0,0 +1,231 @@
package main
import (
"crypto/rand"
"database/sql"
"fmt"
"html/template"
"io"
"log"
"math/big"
"net/http"
"net/url"
"os"
"github.com/BurntSushi/toml"
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
)
var tmpl = template.Must(template.ParseFiles("./sites/index.html"))
var statsTmpl = template.Must(template.ParseFiles("./sites/stats.html"))
var successTmpl = template.Must(template.ParseFiles("./sites/success.html"))
type SuccessShortend struct {
LongLink string
ShortLink string
Success bool
Error bool
}
// GenerateRandomString returns a securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret[i] = letters[num.Int64()]
}
return string(ret), nil
}
func HandleStats(w http.ResponseWriter, r *http.Request) {
var TotalSaved string
query := `SELECT * FROM TotalSaved`
err := db.QueryRow(query).Scan(&TotalSaved)
if err != nil {
statsTmpl.Execute(w, TotalSaved)
return
}
statsTmpl.Execute(w, TotalSaved)
}
func AboutPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./sites/about.html")
}
func UnshortenHandler(w http.ResponseWriter, r *http.Request) {
token := mux.Vars(r)["token"]
var unshortenedLink string
query := `SELECT oldLink FROM redirects where newLink = BINARY ?`
err := db.QueryRow(query, token).Scan(&unshortenedLink)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "404 not found")
return
}
http.Redirect(w, r, unshortenedLink, http.StatusSeeOther)
}
func IndexHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
tmpl.Execute(w, nil)
return
}
link := r.FormValue("userLink")
println(link)
u, err := url.ParseRequestURI(string(link))
if err != nil {
tmpl.Execute(w, struct{ Error bool }{Error: true})
return
}
// If no errors Occured
s := SuccessShortend{
LongLink: u.String(),
ShortLink: "",
Success: false,
Error: false,
}
var token string
query := `SELECT newLink FROM redirects where oldLink = ?`
err = db.QueryRow(query, string(link)).Scan(&token)
if err == nil {
s.ShortLink = "forkort.dk/" + token
successTmpl.Execute(w, s)
return
}
CREATE_TOKEN:
token, err = GenerateRandomString(4)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Server Error")
return
}
query = `SELECT newLink FROM redirects where newLink = BINARY ?`
err = db.QueryRow(query, token).Scan(&token)
if err == nil {
goto CREATE_TOKEN
}
s.ShortLink = "forkort.dk/" + token
_, err = db.Exec(`INSERT INTO redirects(oldLink, newLink) VALUES (?, ?)`, string(link), token)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
tmpl.Execute(w, struct{ Error bool }{Error: true})
return
}
successTmpl.Execute(w, s)
}
func UnshortenApi(w http.ResponseWriter, r *http.Request) {
token := mux.Vars(r)["token"]
var unshortenedLink string
query := `SELECT oldLink FROM redirects where newLink = BINARY ?`
err := db.QueryRow(query, token).Scan(&unshortenedLink)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "404 not found")
return
}
w.Write([]byte(unshortenedLink))
}
func ShortenApi(w http.ResponseWriter, r *http.Request) {
link, err := io.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
u, err := url.ParseRequestURI(string(link))
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Invalid URL")
return
}
log.Println(u)
log.Println(string(link))
var token string
query := `SELECT newLink FROM redirects where oldLink = ?`
err = db.QueryRow(query, string(link)).Scan(&token)
if err == nil {
fmt.Fprintf(w, "forkort.dk/"+token)
return
}
CREATE_TOKEN:
token, err = GenerateRandomString(4)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Server Error")
}
query = `SELECT newLink FROM redirects where newLink = BINARY ?`
err = db.QueryRow(query, token).Scan(&token)
if err == nil {
goto CREATE_TOKEN
}
_, err = db.Exec(`INSERT INTO redirects(oldLink, newLink) VALUES (?, ?)`, string(link), token)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Bad request")
return
}
fmt.Fprintf(w, "forkort.dk/"+token)
}
var db *sql.DB
type Config struct {
MYSQL_DB string
MYSQL_USER string
MYSQL_PASS string
MYSQL_HOST string
}
func main() {
var cfg Config
file, err := os.ReadFile("./config.toml")
if err != nil {
log.Fatal("you need a config.toml file")
}
err = toml.Unmarshal(file, &cfg)
if err != nil {
panic(err)
}
db, err = sql.Open("mysql", cfg.MYSQL_USER+":@("+cfg.MYSQL_HOST+"/"+cfg.MYSQL_DB+"?parseTime=true")
if err != nil {
log.Fatal(err)
}
err = db.Ping()
if err != nil {
log.Fatal(err)
}
defer db.Close()
log.Println("Database connection established")
router := mux.NewRouter()
router.HandleFunc("/", IndexHandler)
router.HandleFunc("/stats", HandleStats)
router.HandleFunc("/about", AboutPage)
router.HandleFunc("/api/shorten", ShortenApi)
router.HandleFunc("/api/unshorten/{token}", UnshortenApi)
router.HandleFunc(`/{token}`, UnshortenHandler)
router.PathPrefix("/static/").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./assets"))))
http.ListenAndServe(":8080", router)
}

15
sites/about.html Normal file
View File

@ -0,0 +1,15 @@
<html>
<head>
<link rel="stylesheet" href="/static/style.css">
</head>
<ul>
<li><a href="/">Hjem</a></li>
<li><a href="/stats">Statistikker</a></li>
<li class="active"><a href="/about">Omkring</a></li>
</ul>
<body style="text-align: center;">
<h1>Omkring</h1>
<br>
<p>Denne side blev startet som et skole projekt og var originalt skrevet i PHP men er siden omskrevet i Go</p>
</body>
</html>

15
sites/forms.html Normal file
View File

@ -0,0 +1,15 @@
<!-- forms.html -->
{{if .Success}}
<h1>Thanks for your message!</h1>
{{else}}
<h1>Contact</h1>
<form method="POST">
<label>Email:</label><br />
<input type="text" name="email"><br />
<label>Subject:</label><br />
<input type="text" name="subject"><br />
<label>Message:</label><br />
<textarea name="message"></textarea><br />
<input type="submit">
</form>
{{end}}

29
sites/index.html Normal file
View File

@ -0,0 +1,29 @@
<html>
<head>
<title>Forkort.dk - Forkort dine links</title>
<link rel="stylesheet" href="/static/style.css">
<meta name="description" content="Forkort.dk gør dine lange links korte.">
</head>
<body>
<ul>
<li class="active"><a href="/">Hjem</a></li>
<li><a href="/stats">Statistikker</a></li>
<li><a href="/about">Omkring</a></li>
</ul>
<h1> Forkort.dk - Forkort dine links</h1>
<h2><i>Altid uden Reklamer og Trackere</i></h2>
<div class="main">
<form method="POST">
<!-- <p>Indtast link du ønsker at forkorte i boksen nedenfor: </p> -->
<label>Indtast dit lange link:</label><br />
<input type="text" name="userLink" id="userLink" value="">
<button type="submit">Indsend</button>
</form>
{{if .Error}}
<p>Indsend et gyldigt fuldt URL med http/https foran linket</p>
{{end}}
<br>
</div>
</body>
</html>

17
sites/stats.html Normal file
View File

@ -0,0 +1,17 @@
<html>
<head>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<ul>
<li><a href="/">Hjem</a></li>
<li class="active"><a href="/stats">Statistikker</a></li>
<li><a href="/about">Omkring</a></li>
</ul>
<h1> Statistikker </h1>
<br>
<div class="main">
<p>Total Mængde tegn sparet: {{.}}</p>
</div>
</body>
</html>

28
sites/success.html Normal file
View File

@ -0,0 +1,28 @@
<html>
<head>
<link rel="stylesheet" href="/static/style.css">
</head>
<script>
function copyToClipboard() {
// Copy the text inside the text field
navigator.clipboard.writeText("https://" + {{.ShortLink}});
// Alert the copied text
alert("Copied the text: " + copyText.value);
}
</script>
<body>
<ul>
<li><a href="/">Hjem</a></li>
<li><a href="/stats">Statistikker</a></li>
<li><a href="/about">Omkring</a></li>
</ul>
<h1> Link forkortet </h1>
<br>
<div class="main">
<p>Dit link: <a href="{{.LongLink}}">{{.LongLink}}</a></p>
<p>Er blevet forkortet</p>
<p>Kort link: <a href="{{.ShortLink}}">{{.ShortLink}} </a><a onclick="copyToClipboard()" href="javascript:void(0);"><svg width="20px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M384 112v352c0 26.51-21.49 48-48 48H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h80c0-35.29 28.71-64 64-64s64 28.71 64 64h80c26.51 0 48 21.49 48 48zM192 40c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24m96 114v-20a6 6 0 0 0-6-6H102a6 6 0 0 0-6 6v20a6 6 0 0 0 6 6h180a6 6 0 0 0 6-6z"/></svg></a></p>
</div>
</body>
</html>