blob: b1db1f9e0df63d48d4bcab3835cc0ea23b9772c0 [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")
22
23//go:embed index.html
24var indexHTML embed.FS
25
Davit Tabidzef99bc4f2024-01-17 22:37:32 +040026//go:embed static/*
27var f embed.FS
28
DTabidzeb00a1db2024-01-12 18:30:14 +040029type NamedAddress struct {
30 Name string
31 Address string
32 OwnerId string
33 Active bool
34}
35
36type Store interface {
37 Create(addr NamedAddress) error
38 Get(name string) (NamedAddress, error)
DTabidze71353b52024-01-17 16:02:55 +040039 UpdateStatus(name string, active bool) error
DTabidzeb00a1db2024-01-12 18:30:14 +040040 ChangeOwner(name, ownerId string) error
41 List(ownerId string) ([]NamedAddress, error)
42}
43
44type NameAlreadyTaken struct {
45 Name string
46}
47
48func (er NameAlreadyTaken) Error() string {
49 return fmt.Sprintf("Name '%s' is already taken", er.Name)
50}
51
52type SQLiteStore struct {
53 db *sql.DB
54}
55
56func NewSQLiteStore(path string) (*SQLiteStore, error) {
57 db, err := sql.Open("sqlite3", path)
58 if err != nil {
59 return nil, err
60 }
61
62 _, err = db.Exec(`
63 CREATE TABLE IF NOT EXISTS named_addresses (
64 name TEXT PRIMARY KEY,
65 address TEXT,
66 owner_id TEXT,
67 active BOOLEAN
68 )
69 `)
70 if err != nil {
71 return nil, err
72 }
73
74 return &SQLiteStore{db: db}, nil
75}
76
77func generateRandomURL() string {
78 const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
79 var urlShort string
80 for i := 0; i < 6; i++ {
81 urlShort += string(charset[rand.Intn(len(charset))])
82 }
83 return urlShort
84}
85
86func (s *SQLiteStore) Create(addr NamedAddress) error {
87 _, err := s.db.Exec(`
88 INSERT INTO named_addresses (name, address, owner_id, active)
89 VALUES (?, ?, ?, ?)
90 `, addr.Name, addr.Address, addr.OwnerId, addr.Active)
91 if err != nil {
Giorgi Lekveishvilicefecf12024-02-07 16:15:29 +040092 sqliteErr, ok := err.(*sqlite3.Error)
DTabidzeb00a1db2024-01-12 18:30:14 +040093 // sqliteErr.ExtendedCode and sqlite3.ErrConstraintUnique are not the same. probably some lib error.
94 // had to use actual code of unique const error
Giorgi Lekveishvilicefecf12024-02-07 16:15:29 +040095 if ok && sqliteErr.ExtendedCode() == 1555 {
DTabidzeb00a1db2024-01-12 18:30:14 +040096 return NameAlreadyTaken{Name: addr.Name}
97 }
98 return err
99 }
100 return nil
101}
102
103func (s *SQLiteStore) Get(name string) (NamedAddress, error) {
104 row := s.db.QueryRow("SELECT name, address, owner_id, active FROM named_addresses WHERE name = ?", name)
105 namedAddress := NamedAddress{}
106 err := row.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active)
107 if err != nil {
108 if err == sql.ErrNoRows {
109 return NamedAddress{}, fmt.Errorf("No record found for name %s", name)
110 }
111 return NamedAddress{}, err
112 }
113 return namedAddress, nil
114}
115
DTabidze71353b52024-01-17 16:02:55 +0400116func (s *SQLiteStore) UpdateStatus(name string, active bool) error {
DTabidzeb00a1db2024-01-12 18:30:14 +0400117 //TODO
DTabidze71353b52024-01-17 16:02:55 +0400118 _, err := s.db.Exec("UPDATE named_addresses SET active = ? WHERE name = ?", active, name)
119 if err != nil {
120 return err
121 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400122 return nil
123}
124
125func (s *SQLiteStore) ChangeOwner(name, ownerId string) error {
126 //TODO
127 return nil
128}
129
130func (s *SQLiteStore) List(ownerId string) ([]NamedAddress, error) {
131 rows, err := s.db.Query("SELECT name, address, owner_id, active FROM named_addresses WHERE owner_id = ?", ownerId)
132 if err != nil {
133 return nil, err
134 }
135 defer rows.Close()
136 var namedAddresses []NamedAddress
137 for rows.Next() {
138 var namedAddress NamedAddress
139 if err := rows.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active); err != nil {
140 return nil, err
141 }
142 namedAddresses = append(namedAddresses, namedAddress)
143 }
144 return namedAddresses, nil
145}
146
147type PageVariables struct {
148 NamedAddresses []NamedAddress
149}
150
151func renderHTML(w http.ResponseWriter, r *http.Request, tpl *template.Template, data interface{}) {
152 w.Header().Set("Content-Type", "text/html")
153 err := tpl.Execute(w, data)
154 if err != nil {
155 http.Error(w, err.Error(), http.StatusInternalServerError)
156 }
157}
158
DTabidze71353b52024-01-17 16:02:55 +0400159func getLoggedInUser(r *http.Request) (string, error) {
160 // TODO(dato): should make a request to get loggedin user
161 return "tabo", nil
162}
163
DTabidzeb00a1db2024-01-12 18:30:14 +0400164type Server struct {
165 store Store
166}
167
168func (s *Server) Start() {
Davit Tabidzef99bc4f2024-01-17 22:37:32 +0400169 http.Handle("/static/", http.FileServer(http.FS(f)))
DTabidzeb00a1db2024-01-12 18:30:14 +0400170 http.HandleFunc("/", s.handler)
DTabidze71353b52024-01-17 16:02:55 +0400171 http.HandleFunc("/api/update/", s.toggleHandler)
Davit Tabidzef99bc4f2024-01-17 22:37:32 +0400172 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
DTabidzeb00a1db2024-01-12 18:30:14 +0400173}
174
175func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
DTabidze71353b52024-01-17 16:02:55 +0400176 loggedInUser, err := getLoggedInUser(r)
177 if err != nil {
178 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
179 return
180 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400181 if r.Method == http.MethodPost {
182 customName := r.PostFormValue("custom")
183 address := r.PostFormValue("address")
184 if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
185 http.Error(w, "Address must start with http:// or https://", http.StatusBadRequest)
186 return
187 }
188 for {
189 cn := customName
190 if cn == "" {
191 cn = generateRandomURL()
192 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400193 namedAddress := NamedAddress{
194 Name: cn,
195 Address: address,
DTabidze71353b52024-01-17 16:02:55 +0400196 OwnerId: loggedInUser,
DTabidzeb00a1db2024-01-12 18:30:14 +0400197 Active: true,
198 }
199 if err := s.store.Create(namedAddress); err == nil {
200 http.Redirect(w, r, "/", http.StatusSeeOther)
201 return
202 } else if _, ok := err.(NameAlreadyTaken); ok && customName == "" {
203 continue
204 } else if _, ok := err.(NameAlreadyTaken); ok && customName != "" {
205 http.Error(w, "Name is already taken", http.StatusBadRequest)
206 return
207 } else {
208 http.Error(w, "Try again later", http.StatusInternalServerError)
209 return
210 }
211 }
212 }
213 // Get Name from request path for redirection
214 name := strings.TrimPrefix(r.URL.Path, "/")
215 if name != "" {
216 namedAddress, err := s.store.Get(name)
217 if err != nil {
218 return
219 }
DTabidze71353b52024-01-17 16:02:55 +0400220 if !namedAddress.Active {
221 http.Error(w, "address not found", http.StatusNotFound)
222 return
223 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400224 http.Redirect(w, r, namedAddress.Address, http.StatusSeeOther)
225 return
226 }
DTabidze71353b52024-01-17 16:02:55 +0400227 namedAddresses, err := s.store.List(loggedInUser)
DTabidzeb00a1db2024-01-12 18:30:14 +0400228 if err != nil {
229 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
230 return
231 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400232 pageVariables := PageVariables{
233 NamedAddresses: namedAddresses,
234 }
235 indexHtmlContent, err := indexHTML.ReadFile("index.html")
236 if err != nil {
237 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
238 return
239 }
240 tmpl, err := template.New("index").Parse(string(indexHtmlContent))
241 if err != nil {
242 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
243 return
244 }
245 renderHTML(w, r, tmpl, pageVariables)
246}
247
DTabidze71353b52024-01-17 16:02:55 +0400248type UpdateRequest struct {
249 Name string `json:"name"`
250 Active bool `json:"active"`
251}
252
253func (s *Server) toggleHandler(w http.ResponseWriter, r *http.Request) {
254 var data UpdateRequest
255 if r.Method == http.MethodPost {
256 loggedInUser, err := getLoggedInUser(r)
257 if err != nil {
258 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
259 return
260 }
261 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
262 http.Error(w, "Failed to decode JSON data", http.StatusBadRequest)
263 return
264 }
265 namedAddress, err := s.store.Get(data.Name)
266 if err != nil {
267 http.Error(w, fmt.Sprintf("Failed to get named_address for name %s", data.Name), http.StatusInternalServerError)
268 return
269 }
270 if namedAddress.OwnerId != loggedInUser {
271 http.Error(w, "Invalid owner ID", http.StatusUnauthorized)
272 return
273 }
274 if err := s.store.UpdateStatus(data.Name, data.Active); err != nil {
275 http.Error(w, fmt.Sprintf("Failed to update status for name %s", data.Name), http.StatusInternalServerError)
276 return
277 }
278 } else {
279 http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
280 return
281 }
282}
283
DTabidzeb00a1db2024-01-12 18:30:14 +0400284func main() {
285 flag.Parse()
286 db, err := NewSQLiteStore(*dbPath)
287 if err != nil {
288 panic(err)
289 }
290 s := Server{store: db}
291 s.Start()
292}