Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): move from react-router to @tanstack/router #1338

Merged
merged 50 commits into from Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4da1e6d
fix(auth): invalid cookie handling and wrongful basic auth invalidation
martylukyy Jan 1, 2024
c212da8
fix(auth): fix test to reflect new HTTP status code
martylukyy Jan 1, 2024
171ea93
fix(auth/web): do not throw on error
martylukyy Jan 2, 2024
8083967
fix(http): replace http codes in middleware to prevent basic auth inv…
martylukyy Jan 2, 2024
7e9bb19
fix test
martylukyy Jan 2, 2024
17a705b
Merge branch 'develop' into fix/auth/cookie-handling-and-basic-auth
zze0s Jan 2, 2024
ed674e8
fix(web): api client handle 403
zze0s Jan 2, 2024
0c527ef
refactor(http): auth_test use testify.assert
zze0s Jan 2, 2024
cc13001
refactor(http): set session opts after valid login
zze0s Jan 2, 2024
a7fabc1
Merge branch 'develop' into fix/auth/cookie-handling-and-basic-auth
kaiserbh Jan 3, 2024
09b86e8
refactor(http): send more client headers
zze0s Jan 3, 2024
0ee395e
fix(http): test
zze0s Jan 3, 2024
47423a6
refactor(web): move router to tanstack/router
zze0s Jan 4, 2024
e671f16
refactor(web): use route loaders and suspense
zze0s Jan 5, 2024
316be1c
refactor(web): useSuspense for settings
zze0s Jan 12, 2024
75c8b49
refactor(web): invalidate cookie in middleware
zze0s Jan 12, 2024
e769764
Merge branch 'develop' into fix/auth/cookie-handling-and-basic-auth
zze0s Feb 5, 2024
b18ba9a
fix: loclfile
zze0s Feb 5, 2024
ceca826
fix: load filter/id
zze0s Feb 7, 2024
f41b543
fix(web): login, onboard, types, imports
zze0s Feb 7, 2024
e57fd4a
fix(web): filter load
zze0s Feb 7, 2024
7ed5d8a
fix(web): build errors
zze0s Feb 8, 2024
4774923
fix(web): ts-expect-error
zze0s Feb 8, 2024
f618433
fix(tests): filter_test.go
zze0s Feb 8, 2024
4ba183c
fix(filters): tests
zze0s Feb 8, 2024
6bbebf1
Merge branch 'develop' into fix/auth/cookie-handling-and-basic-auth
zze0s Feb 8, 2024
6e08c1a
refactor: remove duplicate spinner components
martylukyy Feb 10, 2024
7a745a1
fix: refactor missed SectionLoader to RingResizeSpinner
martylukyy Feb 10, 2024
cb6744d
fix: substitute divides with borders to account for unloaded elements
martylukyy Feb 10, 2024
b425e1b
fix(api): action status URL param
martylukyy Feb 10, 2024
67a055f
revert: action status URL param
martylukyy Feb 10, 2024
d3e926b
fix(routing): notfound handling and split files
zze0s Feb 11, 2024
6c03e8c
fix(filters): notfound get params
zze0s Feb 11, 2024
f1a117d
fix(queries): colon
zze0s Feb 11, 2024
d849efd
fix(queries): comments ts-ignore
zze0s Feb 11, 2024
c69ae5e
fix(queries): extract queryKeys
zze0s Feb 11, 2024
2970df4
fix(queries): remove err
zze0s Feb 11, 2024
63b985d
fix(routes): move zob schema inline
zze0s Feb 11, 2024
66042f2
fix(auth): middleware and redirect to login
zze0s Feb 11, 2024
a961835
fix(auth): failing test
zze0s Feb 11, 2024
0373323
fix(logs): invalidate correct key
zze0s Feb 11, 2024
26ae66c
fix(logs): invalidate correct key
zze0s Feb 11, 2024
815e13a
fix(logs): invalidate correct key
zze0s Feb 11, 2024
404e736
fix: JSX element stealing focus from searchbar
martylukyy Feb 11, 2024
048b500
reimplement empty release table state text
martylukyy Feb 11, 2024
83fbc72
fix(context): use deep-copy
zze0s Feb 12, 2024
889408a
fix(releases): empty state and filter input warnings
zze0s Feb 12, 2024
e39c122
fix(releases): empty states
zze0s Feb 12, 2024
30d04d9
fix(auth): onboarding
zze0s Feb 12, 2024
b8b135a
fix(cache): invalidate queries
zze0s Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 18 additions & 17 deletions internal/http/auth.go
Expand Up @@ -66,36 +66,37 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) {
return
}

h.cookieStore.Options.HttpOnly = true
h.cookieStore.Options.SameSite = http.SameSiteLaxMode
h.cookieStore.Options.Path = h.config.BaseURL

// autobrr does not support serving on TLS / https, so this is only available behind reverse proxy
// if forwarded protocol is https then set cookie secure
// SameSite Strict can only be set with a secure cookie. So we overwrite it here if possible.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
if r.Header.Get("X-Forwarded-Proto") == "https" {
h.cookieStore.Options.Secure = true
h.cookieStore.Options.SameSite = http.SameSiteStrictMode
}

if _, err := h.service.Login(r.Context(), data.Username, data.Password); err != nil {
h.log.Error().Err(err).Msgf("Auth: Failed login attempt username: [%s] ip: %s", data.Username, r.RemoteAddr)
h.encoder.StatusError(w, http.StatusUnauthorized, errors.New("could not login: bad credentials"))
h.encoder.StatusError(w, http.StatusForbidden, errors.New("could not login: bad credentials"))
return
}

// create new session
session, err := h.cookieStore.New(r, "user_session")
session, err := h.cookieStore.Get(r, "user_session")
if err != nil {
h.log.Error().Err(err).Msgf("Auth: Failed to parse cookies with attempt username: [%s] ip: %s", data.Username, r.RemoteAddr)
h.encoder.StatusError(w, http.StatusUnauthorized, errors.New("could not parse cookies"))
h.log.Error().Err(err).Msgf("could not get session from cookieStore: %s", r.RemoteAddr)
h.encoder.StatusError(w, http.StatusInternalServerError, err)
return
}

// Set user as authenticated
session.Values["authenticated"] = true

// Set cookie options
session.Options.HttpOnly = true
session.Options.SameSite = http.SameSiteLaxMode
session.Options.Path = h.config.BaseURL

// autobrr does not support serving on TLS / https, so this is only available behind reverse proxy
// if forwarded protocol is https then set cookie secure
// SameSite Strict can only be set with a secure cookie. So we overwrite it here if possible.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
if r.Header.Get("X-Forwarded-Proto") == "https" {
session.Options.Secure = true
session.Options.SameSite = http.SameSiteStrictMode
}

if err := session.Save(r, w); err != nil {
h.encoder.StatusError(w, http.StatusInternalServerError, errors.Wrap(err, "could not save session"))
return
Expand Down
33 changes: 11 additions & 22 deletions internal/http/auth_test.go
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)

type authServiceMock struct {
Expand Down Expand Up @@ -144,9 +145,7 @@ func TestAuthHandlerLogin(t *testing.T) {
defer resp.Body.Close()

// check for response, here we'll just check for 204 NoContent
if status := resp.StatusCode; status != http.StatusNoContent {
t.Errorf("login: handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}
assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "login handler: unexpected http status")

if v := resp.Header.Get("Set-Cookie"); v == "" {
t.Errorf("handler returned no cookie")
Expand Down Expand Up @@ -207,12 +206,10 @@ func TestAuthHandlerValidateOK(t *testing.T) {
defer resp.Body.Close()

// check for response, here we'll just check for 204 NoContent
if status := resp.StatusCode; status != http.StatusNoContent {
t.Errorf("login: handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}
assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "login handler: bad response")

if v := resp.Header.Get("Set-Cookie"); v == "" {
t.Errorf("handler returned no cookie")
assert.Equalf(t, "", v, "login handler: expected Set-Cookie header")
}

// validate token
Expand All @@ -223,9 +220,7 @@ func TestAuthHandlerValidateOK(t *testing.T) {

defer resp.Body.Close()

if status := resp.StatusCode; status != http.StatusNoContent {
t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}
assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "validate handler: unexpected http status")
}

func TestAuthHandlerValidateBad(t *testing.T) {
Expand Down Expand Up @@ -272,9 +267,7 @@ func TestAuthHandlerValidateBad(t *testing.T) {

defer resp.Body.Close()

if status := resp.StatusCode; status != http.StatusUnauthorized {
t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized)
}
assert.Equalf(t, http.StatusForbidden, resp.StatusCode, "validate handler: unexpected http status")
}

func TestAuthHandlerLoginBad(t *testing.T) {
Expand Down Expand Up @@ -321,9 +314,7 @@ func TestAuthHandlerLoginBad(t *testing.T) {
defer resp.Body.Close()

// check for response, here we'll just check for 204 NoContent
if status := resp.StatusCode; status != http.StatusUnauthorized {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized)
}
assert.Equalf(t, http.StatusForbidden, resp.StatusCode, "login handler: unexpected http status")
}

func TestAuthHandlerLogout(t *testing.T) {
Expand Down Expand Up @@ -384,6 +375,8 @@ func TestAuthHandlerLogout(t *testing.T) {
t.Errorf("login: handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}

assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "login handler: unexpected http status")

if v := resp.Header.Get("Set-Cookie"); v == "" {
t.Errorf("handler returned no cookie")
}
Expand All @@ -396,9 +389,7 @@ func TestAuthHandlerLogout(t *testing.T) {

defer resp.Body.Close()

if status := resp.StatusCode; status != http.StatusNoContent {
t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}
assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "validate handler: unexpected http status")

// logout
resp, err = client.Post(testServer.URL+"/auth/logout", "application/json", nil)
Expand All @@ -408,9 +399,7 @@ func TestAuthHandlerLogout(t *testing.T) {

defer resp.Body.Close()

if status := resp.StatusCode; status != http.StatusNoContent {
t.Errorf("logout: handler returned wrong status code: got %v want %v", status, http.StatusNoContent)
}
assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "logout handler: unexpected http status")

//if v := resp.Header.Get("Set-Cookie"); v != "" {
// t.Errorf("logout handler returned cookie")
Expand Down
8 changes: 4 additions & 4 deletions internal/http/middleware.go
Expand Up @@ -24,7 +24,7 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler {
}

} else if key := r.URL.Query().Get("apikey"); key != "" {
// check query param lke ?apikey=TOKEN
// check query param like ?apikey=TOKEN
if !s.apiService.ValidateAPIKey(r.Context(), key) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
Expand All @@ -33,18 +33,18 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler {
// check session
session, err := s.cookieStore.Get(r, "user_session")
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

if session.IsNew {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}

// Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}

Expand Down
2 changes: 1 addition & 1 deletion web/src/App.tsx
Expand Up @@ -21,7 +21,7 @@ const queryClient = new QueryClient({
// See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay
// delay = Math.min(1000 * 2 ** attemptIndex, 30000)
retry: true,
throwOnError: true,
throwOnError: false,
},
mutations: {
onError: (error) => {
Expand Down
9 changes: 7 additions & 2 deletions web/src/api/APIClient.ts
Expand Up @@ -89,10 +89,15 @@ export async function HttpClient<T = unknown>(
case 401: {
// Remove auth info from localStorage
AuthContext.reset();
}

// Show an error toast to notify the user what occurred
return Promise.reject(new Error(`[401] Unauthorized: "${endpoint}"`));
}
case 403: {
// Remove auth info from localStorage
AuthContext.reset();
// Show an error toast to notify the user what occurred
return Promise.reject(new Error(`[403] Forbidden: "${endpoint}"`));
}
case 404: {
return Promise.reject(new Error(`[404] Not found: "${endpoint}"`));
}
Expand Down