blob: bb805e4688e3663887e70ce3d14a49c5339c8113 [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) {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400160 if user := r.Header.Get("X-User"); user != "" {
161 return user, nil
162 } else {
163 return "", fmt.Errorf("unauthenticated")
164 }
DTabidze71353b52024-01-17 16:02:55 +0400165}
166
DTabidzeb00a1db2024-01-12 18:30:14 +0400167type Server struct {
168 store Store
169}
170
171func (s *Server) Start() {
Davit Tabidzef99bc4f2024-01-17 22:37:32 +0400172 http.Handle("/static/", http.FileServer(http.FS(f)))
DTabidzeb00a1db2024-01-12 18:30:14 +0400173 http.HandleFunc("/", s.handler)
DTabidze71353b52024-01-17 16:02:55 +0400174 http.HandleFunc("/api/update/", s.toggleHandler)
Davit Tabidzef99bc4f2024-01-17 22:37:32 +0400175 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
DTabidzeb00a1db2024-01-12 18:30:14 +0400176}
177
178func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
DTabidze71353b52024-01-17 16:02:55 +0400179 loggedInUser, err := getLoggedInUser(r)
180 if err != nil {
181 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
182 return
183 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400184 if r.Method == http.MethodPost {
185 customName := r.PostFormValue("custom")
186 address := r.PostFormValue("address")
187 if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
188 http.Error(w, "Address must start with http:// or https://", http.StatusBadRequest)
189 return
190 }
191 for {
192 cn := customName
193 if cn == "" {
194 cn = generateRandomURL()
195 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400196 namedAddress := NamedAddress{
197 Name: cn,
198 Address: address,
DTabidze71353b52024-01-17 16:02:55 +0400199 OwnerId: loggedInUser,
DTabidzeb00a1db2024-01-12 18:30:14 +0400200 Active: true,
201 }
202 if err := s.store.Create(namedAddress); err == nil {
203 http.Redirect(w, r, "/", http.StatusSeeOther)
204 return
205 } else if _, ok := err.(NameAlreadyTaken); ok && customName == "" {
206 continue
207 } else if _, ok := err.(NameAlreadyTaken); ok && customName != "" {
208 http.Error(w, "Name is already taken", http.StatusBadRequest)
209 return
210 } else {
211 http.Error(w, "Try again later", http.StatusInternalServerError)
212 return
213 }
214 }
215 }
216 // Get Name from request path for redirection
217 name := strings.TrimPrefix(r.URL.Path, "/")
218 if name != "" {
219 namedAddress, err := s.store.Get(name)
220 if err != nil {
221 return
222 }
DTabidze71353b52024-01-17 16:02:55 +0400223 if !namedAddress.Active {
224 http.Error(w, "address not found", http.StatusNotFound)
225 return
226 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400227 http.Redirect(w, r, namedAddress.Address, http.StatusSeeOther)
228 return
229 }
DTabidze71353b52024-01-17 16:02:55 +0400230 namedAddresses, err := s.store.List(loggedInUser)
DTabidzeb00a1db2024-01-12 18:30:14 +0400231 if err != nil {
232 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
233 return
234 }
DTabidzeb00a1db2024-01-12 18:30:14 +0400235 pageVariables := PageVariables{
236 NamedAddresses: namedAddresses,
237 }
238 indexHtmlContent, err := indexHTML.ReadFile("index.html")
239 if err != nil {
240 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
241 return
242 }
243 tmpl, err := template.New("index").Parse(string(indexHtmlContent))
244 if err != nil {
245 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
246 return
247 }
248 renderHTML(w, r, tmpl, pageVariables)
249}
250
DTabidze71353b52024-01-17 16:02:55 +0400251type UpdateRequest struct {
252 Name string `json:"name"`
253 Active bool `json:"active"`
254}
255
256func (s *Server) toggleHandler(w http.ResponseWriter, r *http.Request) {
257 var data UpdateRequest
258 if r.Method == http.MethodPost {
259 loggedInUser, err := getLoggedInUser(r)
260 if err != nil {
261 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
262 return
263 }
264 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
265 http.Error(w, "Failed to decode JSON data", http.StatusBadRequest)
266 return
267 }
268 namedAddress, err := s.store.Get(data.Name)
269 if err != nil {
270 http.Error(w, fmt.Sprintf("Failed to get named_address for name %s", data.Name), http.StatusInternalServerError)
271 return
272 }
273 if namedAddress.OwnerId != loggedInUser {
274 http.Error(w, "Invalid owner ID", http.StatusUnauthorized)
275 return
276 }
277 if err := s.store.UpdateStatus(data.Name, data.Active); err != nil {
278 http.Error(w, fmt.Sprintf("Failed to update status for name %s", data.Name), http.StatusInternalServerError)
279 return
280 }
281 } else {
282 http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
283 return
284 }
285}
286
DTabidzeb00a1db2024-01-12 18:30:14 +0400287func main() {
288 flag.Parse()
289 db, err := NewSQLiteStore(*dbPath)
290 if err != nil {
291 panic(err)
292 }
293 s := Server{store: db}
294 s.Start()
295}