blob: ff870941c3e8d42c5b0232135035e098b64bfff4 [file] [log] [blame]
DTabidzeb00a1db2024-01-12 18:30:14 +04001package main
2
3import (
4 "database/sql"
5 "embed"
DTabidze71353b52024-01-17 16:02:55 +04006 "encoding/json"
DTabidzeb00a1db2024-01-12 18:30:14 +04007 "flag"
8 "fmt"
9 "html/template"
10 "log"
11 "math/rand"
12 "net/http"
13 "strings"
14
Giorgi Lekveishvilicefecf12024-02-07 16:15:29 +040015 "github.com/ncruces/go-sqlite3"
16 _ "github.com/ncruces/go-sqlite3/driver"
17 _ "github.com/ncruces/go-sqlite3/embed"
DTabidzeb00a1db2024-01-12 18:30:14 +040018)
19
Davit Tabidzef99bc4f2024-01-17 22:37:32 +040020var port = flag.Int("port", 8080, "Port to listen on")
DTabidzeb00a1db2024-01-12 18:30:14 +040021var dbPath = flag.String("db-path", "url-shortener.db", "Path to the SQLite file")
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040022var requireAuth = flag.Bool("require-auth", false, "If false there won't be made any distinctions between users")
23
24const anyUser = "__any__"
DTabidzeb00a1db2024-01-12 18:30:14 +040025
26//go:embed index.html
27var indexHTML embed.FS
28
Davit Tabidzef99bc4f2024-01-17 22:37:32 +040029//go:embed static/*
30var f embed.FS
31
DTabidzeb00a1db2024-01-12 18:30:14 +040032type NamedAddress struct {
33 Name string
34 Address string
35 OwnerId string
36 Active bool
37}
38
39type Store interface {
40 Create(addr NamedAddress) error
41 Get(name string) (NamedAddress, error)
DTabidze71353b52024-01-17 16:02:55 +040042 UpdateStatus(name string, active bool) error
DTabidzeb00a1db2024-01-12 18:30:14 +040043 ChangeOwner(name, ownerId string) error
44 List(ownerId string) ([]NamedAddress, error)
45}
46
47type NameAlreadyTaken struct {
48 Name string
49}
50
51func (er NameAlreadyTaken) Error() string {
52 return fmt.Sprintf("Name '%s' is already taken", er.Name)
53}
54
55type SQLiteStore struct {
56 db *sql.DB
57}
58
59func NewSQLiteStore(path string) (*SQLiteStore, error) {
60 db, err := sql.Open("sqlite3", path)
61 if err != nil {
62 return nil, err
63 }
64
65 _, err = db.Exec(`
66 CREATE TABLE IF NOT EXISTS named_addresses (
67 name TEXT PRIMARY KEY,
68 address TEXT,
69 owner_id TEXT,
70 active BOOLEAN
71 )
72 `)
73 if err != nil {
74 return nil, err
75 }
76
77 return &SQLiteStore{db: db}, nil
78}
79
80func generateRandomURL() string {
81 const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
82 var urlShort string
83 for i := 0; i < 6; i++ {
84 urlShort += string(charset[rand.Intn(len(charset))])
85 }
86 return urlShort
87}
88
89func (s *SQLiteStore) Create(addr NamedAddress) error {
90 _, err := s.db.Exec(`
91 INSERT INTO named_addresses (name, address, owner_id, active)
92 VALUES (?, ?, ?, ?)
93 `, addr.Name, addr.Address, addr.OwnerId, addr.Active)
94 if err != nil {
Giorgi Lekveishvilicefecf12024-02-07 16:15:29 +040095 sqliteErr, ok := err.(*sqlite3.Error)
DTabidzeb00a1db2024-01-12 18:30:14 +040096 // sqliteErr.ExtendedCode and sqlite3.ErrConstraintUnique are not the same. probably some lib error.
97 // had to use actual code of unique const error
Giorgi Lekveishvilicefecf12024-02-07 16:15:29 +040098 if ok && sqliteErr.ExtendedCode() == 1555 {
DTabidzeb00a1db2024-01-12 18:30:14 +040099 return NameAlreadyTaken{Name: addr.Name}
100 }
101 return err
102 }
103 return nil
104}
105
106func (s *SQLiteStore) Get(name string) (NamedAddress, error) {
107 row := s.db.QueryRow("SELECT name, address, owner_id, active FROM named_addresses WHERE name = ?", name)
108 namedAddress := NamedAddress{}
109 err := row.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active)
110 if err != nil {
111 if err == sql.ErrNoRows {
112 return NamedAddress{}, fmt.Errorf("No record found for name %s", name)
113 }
114 return NamedAddress{}, err
115 }
116 return namedAddress, nil
117}
118
DTabidze71353b52024-01-17 16:02:55 +0400119func (s *SQLiteStore) UpdateStatus(name string, active bool) error {
DTabidzeb00a1db2024-01-12 18:30:14 +0400120 //TODO
DTabidze71353b52024-01-17 16:02:55 +0400121 _, err := s.db.Exec("UPDATE named_addresses SET active = ? WHERE name = ?", active, name)
122 if err != nil {
123 return err
124 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400125 return nil
126}
127
128func (s *SQLiteStore) ChangeOwner(name, ownerId string) error {
129 //TODO
130 return nil
131}
132
133func (s *SQLiteStore) List(ownerId string) ([]NamedAddress, error) {
134 rows, err := s.db.Query("SELECT name, address, owner_id, active FROM named_addresses WHERE owner_id = ?", ownerId)
135 if err != nil {
136 return nil, err
137 }
138 defer rows.Close()
139 var namedAddresses []NamedAddress
140 for rows.Next() {
141 var namedAddress NamedAddress
142 if err := rows.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active); err != nil {
143 return nil, err
144 }
145 namedAddresses = append(namedAddresses, namedAddress)
146 }
147 return namedAddresses, nil
148}
149
150type PageVariables struct {
151 NamedAddresses []NamedAddress
152}
153
154func renderHTML(w http.ResponseWriter, r *http.Request, tpl *template.Template, data interface{}) {
155 w.Header().Set("Content-Type", "text/html")
156 err := tpl.Execute(w, data)
157 if err != nil {
158 http.Error(w, err.Error(), http.StatusInternalServerError)
159 }
160}
161
DTabidze71353b52024-01-17 16:02:55 +0400162func getLoggedInUser(r *http.Request) (string, error) {
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400163 if !*requireAuth {
164 return anyUser, nil
165 }
giodd213152024-09-27 11:26:59 +0200166 if user := r.Header.Get("X-Forwarded-User"); user != "" {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400167 return user, nil
168 } else {
169 return "", fmt.Errorf("unauthenticated")
170 }
DTabidze71353b52024-01-17 16:02:55 +0400171}
172
DTabidzeb00a1db2024-01-12 18:30:14 +0400173type Server struct {
174 store Store
175}
176
177func (s *Server) Start() {
Davit Tabidzef99bc4f2024-01-17 22:37:32 +0400178 http.Handle("/static/", http.FileServer(http.FS(f)))
DTabidzeb00a1db2024-01-12 18:30:14 +0400179 http.HandleFunc("/", s.handler)
DTabidze71353b52024-01-17 16:02:55 +0400180 http.HandleFunc("/api/update/", s.toggleHandler)
Davit Tabidzef99bc4f2024-01-17 22:37:32 +0400181 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
DTabidzeb00a1db2024-01-12 18:30:14 +0400182}
183
184func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
DTabidze71353b52024-01-17 16:02:55 +0400185 loggedInUser, err := getLoggedInUser(r)
186 if err != nil {
187 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
188 return
189 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400190 if r.Method == http.MethodPost {
191 customName := r.PostFormValue("custom")
192 address := r.PostFormValue("address")
193 if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
194 http.Error(w, "Address must start with http:// or https://", http.StatusBadRequest)
195 return
196 }
197 for {
198 cn := customName
199 if cn == "" {
200 cn = generateRandomURL()
201 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400202 namedAddress := NamedAddress{
203 Name: cn,
204 Address: address,
DTabidze71353b52024-01-17 16:02:55 +0400205 OwnerId: loggedInUser,
DTabidzeb00a1db2024-01-12 18:30:14 +0400206 Active: true,
207 }
208 if err := s.store.Create(namedAddress); err == nil {
209 http.Redirect(w, r, "/", http.StatusSeeOther)
210 return
211 } else if _, ok := err.(NameAlreadyTaken); ok && customName == "" {
212 continue
213 } else if _, ok := err.(NameAlreadyTaken); ok && customName != "" {
214 http.Error(w, "Name is already taken", http.StatusBadRequest)
215 return
216 } else {
217 http.Error(w, "Try again later", http.StatusInternalServerError)
218 return
219 }
220 }
221 }
222 // Get Name from request path for redirection
223 name := strings.TrimPrefix(r.URL.Path, "/")
224 if name != "" {
225 namedAddress, err := s.store.Get(name)
226 if err != nil {
227 return
228 }
DTabidze71353b52024-01-17 16:02:55 +0400229 if !namedAddress.Active {
230 http.Error(w, "address not found", http.StatusNotFound)
231 return
232 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400233 http.Redirect(w, r, namedAddress.Address, http.StatusSeeOther)
234 return
235 }
DTabidze71353b52024-01-17 16:02:55 +0400236 namedAddresses, err := s.store.List(loggedInUser)
DTabidzeb00a1db2024-01-12 18:30:14 +0400237 if err != nil {
238 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
239 return
240 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400241 pageVariables := PageVariables{
242 NamedAddresses: namedAddresses,
243 }
244 indexHtmlContent, err := indexHTML.ReadFile("index.html")
245 if err != nil {
246 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
247 return
248 }
249 tmpl, err := template.New("index").Parse(string(indexHtmlContent))
250 if err != nil {
251 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
252 return
253 }
254 renderHTML(w, r, tmpl, pageVariables)
255}
256
DTabidze71353b52024-01-17 16:02:55 +0400257type UpdateRequest struct {
258 Name string `json:"name"`
259 Active bool `json:"active"`
260}
261
262func (s *Server) toggleHandler(w http.ResponseWriter, r *http.Request) {
263 var data UpdateRequest
264 if r.Method == http.MethodPost {
265 loggedInUser, err := getLoggedInUser(r)
266 if err != nil {
267 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
268 return
269 }
270 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
271 http.Error(w, "Failed to decode JSON data", http.StatusBadRequest)
272 return
273 }
274 namedAddress, err := s.store.Get(data.Name)
275 if err != nil {
276 http.Error(w, fmt.Sprintf("Failed to get named_address for name %s", data.Name), http.StatusInternalServerError)
277 return
278 }
279 if namedAddress.OwnerId != loggedInUser {
280 http.Error(w, "Invalid owner ID", http.StatusUnauthorized)
281 return
282 }
283 if err := s.store.UpdateStatus(data.Name, data.Active); err != nil {
284 http.Error(w, fmt.Sprintf("Failed to update status for name %s", data.Name), http.StatusInternalServerError)
285 return
286 }
287 } else {
288 http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
289 return
290 }
291}
292
DTabidzeb00a1db2024-01-12 18:30:14 +0400293func main() {
294 flag.Parse()
295 db, err := NewSQLiteStore(*dbPath)
296 if err != nil {
297 panic(err)
298 }
299 s := Server{store: db}
300 s.Start()
301}