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