blob: 8f2af45179e2d2e59238ed313ae20031e8557b72 [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
15 "github.com/mattn/go-sqlite3"
16)
17
18var dbPath = flag.String("db-path", "url-shortener.db", "Path to the SQLite file")
19
20//go:embed index.html
21var indexHTML embed.FS
22
23type NamedAddress struct {
24 Name string
25 Address string
26 OwnerId string
27 Active bool
28}
29
30type Store interface {
31 Create(addr NamedAddress) error
32 Get(name string) (NamedAddress, error)
DTabidze71353b52024-01-17 16:02:55 +040033 UpdateStatus(name string, active bool) error
DTabidzeb00a1db2024-01-12 18:30:14 +040034 ChangeOwner(name, ownerId string) error
35 List(ownerId string) ([]NamedAddress, error)
36}
37
38type NameAlreadyTaken struct {
39 Name string
40}
41
42func (er NameAlreadyTaken) Error() string {
43 return fmt.Sprintf("Name '%s' is already taken", er.Name)
44}
45
46type SQLiteStore struct {
47 db *sql.DB
48}
49
50func NewSQLiteStore(path string) (*SQLiteStore, error) {
51 db, err := sql.Open("sqlite3", path)
52 if err != nil {
53 return nil, err
54 }
55
56 _, err = db.Exec(`
57 CREATE TABLE IF NOT EXISTS named_addresses (
58 name TEXT PRIMARY KEY,
59 address TEXT,
60 owner_id TEXT,
61 active BOOLEAN
62 )
63 `)
64 if err != nil {
65 return nil, err
66 }
67
68 return &SQLiteStore{db: db}, nil
69}
70
71func generateRandomURL() string {
72 const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
73 var urlShort string
74 for i := 0; i < 6; i++ {
75 urlShort += string(charset[rand.Intn(len(charset))])
76 }
77 return urlShort
78}
79
80func (s *SQLiteStore) Create(addr NamedAddress) error {
81 _, err := s.db.Exec(`
82 INSERT INTO named_addresses (name, address, owner_id, active)
83 VALUES (?, ?, ?, ?)
84 `, addr.Name, addr.Address, addr.OwnerId, addr.Active)
85 if err != nil {
86 sqliteErr, ok := err.(sqlite3.Error)
87 // sqliteErr.ExtendedCode and sqlite3.ErrConstraintUnique are not the same. probably some lib error.
88 // had to use actual code of unique const error
89 if ok && sqliteErr.ExtendedCode == 1555 {
90 return NameAlreadyTaken{Name: addr.Name}
91 }
92 return err
93 }
94 return nil
95}
96
97func (s *SQLiteStore) Get(name string) (NamedAddress, error) {
98 row := s.db.QueryRow("SELECT name, address, owner_id, active FROM named_addresses WHERE name = ?", name)
99 namedAddress := NamedAddress{}
100 err := row.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active)
101 if err != nil {
102 if err == sql.ErrNoRows {
103 return NamedAddress{}, fmt.Errorf("No record found for name %s", name)
104 }
105 return NamedAddress{}, err
106 }
107 return namedAddress, nil
108}
109
DTabidze71353b52024-01-17 16:02:55 +0400110func (s *SQLiteStore) UpdateStatus(name string, active bool) error {
DTabidzeb00a1db2024-01-12 18:30:14 +0400111 //TODO
DTabidze71353b52024-01-17 16:02:55 +0400112 _, err := s.db.Exec("UPDATE named_addresses SET active = ? WHERE name = ?", active, name)
113 if err != nil {
114 return err
115 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400116 return nil
117}
118
119func (s *SQLiteStore) ChangeOwner(name, ownerId string) error {
120 //TODO
121 return nil
122}
123
124func (s *SQLiteStore) List(ownerId string) ([]NamedAddress, error) {
125 rows, err := s.db.Query("SELECT name, address, owner_id, active FROM named_addresses WHERE owner_id = ?", ownerId)
126 if err != nil {
127 return nil, err
128 }
129 defer rows.Close()
130 var namedAddresses []NamedAddress
131 for rows.Next() {
132 var namedAddress NamedAddress
133 if err := rows.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active); err != nil {
134 return nil, err
135 }
136 namedAddresses = append(namedAddresses, namedAddress)
137 }
138 return namedAddresses, nil
139}
140
141type PageVariables struct {
142 NamedAddresses []NamedAddress
143}
144
145func renderHTML(w http.ResponseWriter, r *http.Request, tpl *template.Template, data interface{}) {
146 w.Header().Set("Content-Type", "text/html")
147 err := tpl.Execute(w, data)
148 if err != nil {
149 http.Error(w, err.Error(), http.StatusInternalServerError)
150 }
151}
152
DTabidze71353b52024-01-17 16:02:55 +0400153func getLoggedInUser(r *http.Request) (string, error) {
154 // TODO(dato): should make a request to get loggedin user
155 return "tabo", nil
156}
157
DTabidzeb00a1db2024-01-12 18:30:14 +0400158type Server struct {
159 store Store
160}
161
162func (s *Server) Start() {
163 http.HandleFunc("/", s.handler)
DTabidze71353b52024-01-17 16:02:55 +0400164 http.HandleFunc("/api/update/", s.toggleHandler)
DTabidzeb00a1db2024-01-12 18:30:14 +0400165 log.Fatal(http.ListenAndServe(":8080", nil))
166}
167
168func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
DTabidze71353b52024-01-17 16:02:55 +0400169 loggedInUser, err := getLoggedInUser(r)
170 if err != nil {
171 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
172 return
173 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400174 if r.Method == http.MethodPost {
175 customName := r.PostFormValue("custom")
176 address := r.PostFormValue("address")
177 if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
178 http.Error(w, "Address must start with http:// or https://", http.StatusBadRequest)
179 return
180 }
181 for {
182 cn := customName
183 if cn == "" {
184 cn = generateRandomURL()
185 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400186 namedAddress := NamedAddress{
187 Name: cn,
188 Address: address,
DTabidze71353b52024-01-17 16:02:55 +0400189 OwnerId: loggedInUser,
DTabidzeb00a1db2024-01-12 18:30:14 +0400190 Active: true,
191 }
192 if err := s.store.Create(namedAddress); err == nil {
193 http.Redirect(w, r, "/", http.StatusSeeOther)
194 return
195 } else if _, ok := err.(NameAlreadyTaken); ok && customName == "" {
196 continue
197 } else if _, ok := err.(NameAlreadyTaken); ok && customName != "" {
198 http.Error(w, "Name is already taken", http.StatusBadRequest)
199 return
200 } else {
201 http.Error(w, "Try again later", http.StatusInternalServerError)
202 return
203 }
204 }
205 }
206 // Get Name from request path for redirection
207 name := strings.TrimPrefix(r.URL.Path, "/")
208 if name != "" {
209 namedAddress, err := s.store.Get(name)
210 if err != nil {
211 return
212 }
DTabidze71353b52024-01-17 16:02:55 +0400213 if !namedAddress.Active {
214 http.Error(w, "address not found", http.StatusNotFound)
215 return
216 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400217 http.Redirect(w, r, namedAddress.Address, http.StatusSeeOther)
218 return
219 }
DTabidze71353b52024-01-17 16:02:55 +0400220 namedAddresses, err := s.store.List(loggedInUser)
DTabidzeb00a1db2024-01-12 18:30:14 +0400221 if err != nil {
222 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
223 return
224 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400225 pageVariables := PageVariables{
226 NamedAddresses: namedAddresses,
227 }
228 indexHtmlContent, err := indexHTML.ReadFile("index.html")
229 if err != nil {
230 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
231 return
232 }
233 tmpl, err := template.New("index").Parse(string(indexHtmlContent))
234 if err != nil {
235 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
236 return
237 }
238 renderHTML(w, r, tmpl, pageVariables)
239}
240
DTabidze71353b52024-01-17 16:02:55 +0400241type UpdateRequest struct {
242 Name string `json:"name"`
243 Active bool `json:"active"`
244}
245
246func (s *Server) toggleHandler(w http.ResponseWriter, r *http.Request) {
247 var data UpdateRequest
248 if r.Method == http.MethodPost {
249 loggedInUser, err := getLoggedInUser(r)
250 if err != nil {
251 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
252 return
253 }
254 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
255 http.Error(w, "Failed to decode JSON data", http.StatusBadRequest)
256 return
257 }
258 namedAddress, err := s.store.Get(data.Name)
259 if err != nil {
260 http.Error(w, fmt.Sprintf("Failed to get named_address for name %s", data.Name), http.StatusInternalServerError)
261 return
262 }
263 if namedAddress.OwnerId != loggedInUser {
264 http.Error(w, "Invalid owner ID", http.StatusUnauthorized)
265 return
266 }
267 if err := s.store.UpdateStatus(data.Name, data.Active); err != nil {
268 http.Error(w, fmt.Sprintf("Failed to update status for name %s", data.Name), http.StatusInternalServerError)
269 return
270 }
271 } else {
272 http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
273 return
274 }
275}
276
DTabidzeb00a1db2024-01-12 18:30:14 +0400277func main() {
278 flag.Parse()
279 db, err := NewSQLiteStore(*dbPath)
280 if err != nil {
281 panic(err)
282 }
283 s := Server{store: db}
284 s.Start()
285}