home to your local SPACEGIRL 馃挮
arimelody.space
1package api
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "os"
8 "strings"
9
10 "arimelody-web/controller"
11 "arimelody-web/model"
12)
13
14func Handler(app *model.AppState) http.Handler {
15 mux := http.NewServeMux()
16
17 // TODO: generate API keys on the frontend
18
19 // ARTIST ENDPOINTS
20
21 mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 var artistID = strings.Split(r.URL.Path[1:], "/")[0]
23 artist, err := controller.GetArtist(app.DB, artistID)
24 if err != nil {
25 if strings.Contains(err.Error(), "no rows") {
26 http.NotFound(w, r)
27 return
28 }
29 fmt.Printf("WARN: Error while retrieving artist %s: %s\n", artistID, err)
30 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
31 return
32 }
33
34 switch r.Method {
35 case http.MethodGet:
36 // GET /api/v1/artist/{id}
37 ServeArtist(app, artist).ServeHTTP(w, r)
38 case http.MethodPut:
39 // PUT /api/v1/artist/{id} (admin)
40 requireAccount(UpdateArtist(app, artist)).ServeHTTP(w, r)
41 case http.MethodDelete:
42 // DELETE /api/v1/artist/{id} (admin)
43 requireAccount(DeleteArtist(app, artist)).ServeHTTP(w, r)
44 default:
45 http.NotFound(w, r)
46 }
47 })))
48 mux.Handle("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49 switch r.Method {
50 case http.MethodGet:
51 // GET /api/v1/artist
52 ServeAllArtists(app).ServeHTTP(w, r)
53 case http.MethodPost:
54 // POST /api/v1/artist (admin)
55 requireAccount(CreateArtist(app)).ServeHTTP(w, r)
56 default:
57 http.NotFound(w, r)
58 }
59 }))
60
61 // RELEASE ENDPOINTS
62
63 mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64 var releaseID = strings.Split(r.URL.Path[1:], "/")[0]
65 release, err := controller.GetRelease(app.DB, releaseID, true)
66 if err != nil {
67 if strings.Contains(err.Error(), "no rows") {
68 http.NotFound(w, r)
69 return
70 }
71 fmt.Printf("WARN: Error while retrieving release %s: %s\n", releaseID, err)
72 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
73 return
74 }
75
76 switch r.Method {
77 case http.MethodGet:
78 // GET /api/v1/music/{id}
79 ServeRelease(app, release).ServeHTTP(w, r)
80 case http.MethodPut:
81 // PUT /api/v1/music/{id} (admin)
82 requireAccount(UpdateRelease(app, release)).ServeHTTP(w, r)
83 case http.MethodDelete:
84 // DELETE /api/v1/music/{id} (admin)
85 requireAccount(DeleteRelease(app, release)).ServeHTTP(w, r)
86 default:
87 http.NotFound(w, r)
88 }
89 })))
90 mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
91 switch r.Method {
92 case http.MethodGet:
93 // GET /api/v1/music
94 ServeCatalog(app).ServeHTTP(w, r)
95 case http.MethodPost:
96 // POST /api/v1/music (admin)
97 requireAccount(CreateRelease(app)).ServeHTTP(w, r)
98 default:
99 http.NotFound(w, r)
100 }
101 }))
102
103 // TRACK ENDPOINTS
104
105 mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106 var trackID = strings.Split(r.URL.Path[1:], "/")[0]
107 track, err := controller.GetTrack(app.DB, trackID)
108 if err != nil {
109 if strings.Contains(err.Error(), "no rows") {
110 http.NotFound(w, r)
111 return
112 }
113 fmt.Printf("WARN: Error while retrieving track %s: %s\n", trackID, err)
114 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
115 return
116 }
117
118 switch r.Method {
119 case http.MethodGet:
120 // GET /api/v1/track/{id} (admin)
121 requireAccount(ServeTrack(app, track)).ServeHTTP(w, r)
122 case http.MethodPut:
123 // PUT /api/v1/track/{id} (admin)
124 requireAccount(UpdateTrack(app, track)).ServeHTTP(w, r)
125 case http.MethodDelete:
126 // DELETE /api/v1/track/{id} (admin)
127 requireAccount(DeleteTrack(app, track)).ServeHTTP(w, r)
128 default:
129 http.NotFound(w, r)
130 }
131 })))
132 mux.Handle("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133 switch r.Method {
134 case http.MethodGet:
135 // GET /api/v1/track (admin)
136 requireAccount(ServeAllTracks(app)).ServeHTTP(w, r)
137 case http.MethodPost:
138 // POST /api/v1/track (admin)
139 requireAccount(CreateTrack(app)).ServeHTTP(w, r)
140 default:
141 http.NotFound(w, r)
142 }
143 }))
144
145 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 session, err := getSession(app, r)
147 if err != nil {
148 fmt.Fprintf(os.Stderr, "WARN: Failed to get session: %v\n", err)
149 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
150 return
151 }
152 ctx := context.WithValue(r.Context(), "session", session)
153 mux.ServeHTTP(w, r.WithContext(ctx))
154 })
155}
156
157func requireAccount(next http.Handler) http.Handler {
158 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
159 session := r.Context().Value("session").(*model.Session)
160 if session == nil || session.Account == nil {
161 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
162 return
163 }
164 ctx := context.WithValue(r.Context(), "session", session)
165 next.ServeHTTP(w, r.WithContext(ctx))
166 })
167}
168
169func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
170 var token string
171
172 // check cookies first
173 sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
174 if err != nil && err != http.ErrNoCookie {
175 return nil, fmt.Errorf("Failed to retrieve session cookie: %v\n", err)
176 }
177 if sessionCookie != nil {
178 token = sessionCookie.Value
179 } else {
180 // check Authorization header
181 token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
182 }
183
184 if token == "" { return nil, nil }
185
186 // fetch existing session
187 session, err := controller.GetSession(app.DB, token)
188
189 if err != nil && !strings.Contains(err.Error(), "no rows") {
190 return nil, fmt.Errorf("Failed to retrieve session: %v\n", err)
191 }
192
193 if session != nil {
194 // TODO: consider running security checks here (i.e. user agent mismatches)
195 }
196
197 return session, nil
198}