diff --git a/docker-compose.yml b/docker-compose.yml index e7a5276..a4acf52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: - "9001:9001" environment: - DB_REMOTE=False + - APP_URL= frontend: container_name: ctk-frontend diff --git a/services/backend/requirements.txt b/services/backend/requirements.txt index 8a0793f..52b8034 100644 --- a/services/backend/requirements.txt +++ b/services/backend/requirements.txt @@ -3,6 +3,7 @@ django-cors-headers==3.11.0 django-axes==5.32.0 djangorestframework==3.13.1 djangorestframework-simplejwt==5.1.0 +django-storages==1.13.1 drf-extensions==0.7.1 dj-rest-auth[with_social]==2.2.4 diff --git a/services/backend/src/api/routing.py b/services/backend/src/api/routing.py index 27f2082..72e9f02 100644 --- a/services/backend/src/api/routing.py +++ b/services/backend/src/api/routing.py @@ -2,7 +2,7 @@ from django.urls import include, path from rest_framework_extensions.routers import ExtendedDefaultRouter -from .views import project, generate, user, view +from .views import project, generate, user, view, auth class DefaultRouterPlusPlus(ExtendedDefaultRouter): @@ -20,5 +20,6 @@ api_urls = [ path("generate/", generate.GenerateGenericAPIView.as_view()), path("auth/self/", user.UserGenericAPIView.as_view()), path("auth/", include("dj_rest_auth.urls")), + path("auth/github/", auth.GitHubLogin.as_view(), name="github_login"), path("auth/registration/", include("dj_rest_auth.registration.urls")), ] diff --git a/services/backend/src/api/views/auth.py b/services/backend/src/api/views/auth.py new file mode 100644 index 0000000..55b69d5 --- /dev/null +++ b/services/backend/src/api/views/auth.py @@ -0,0 +1,11 @@ +import os +from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter +from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from dj_rest_auth.registration.views import SocialLoginView + +APP_URL = os.environ.get("APP_URL", "") + +class GitHubLogin(SocialLoginView): + adapter_class = GitHubOAuth2Adapter + callback_url = f"{APP_URL}/github/cb" + client_class = OAuth2Client diff --git a/services/backend/src/main/settings.py b/services/backend/src/main/settings.py index 266f752..3e71290 100644 --- a/services/backend/src/main/settings.py +++ b/services/backend/src/main/settings.py @@ -52,7 +52,9 @@ INSTALLED_APPS = [ "allauth", "allauth.account", "allauth.socialaccount", + "allauth.socialaccount.providers.github", "dj_rest_auth.registration", + "storages", "corsheaders", "axes", "organizations", @@ -152,13 +154,6 @@ USE_I18N = True USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.0/howto/static-files/ - -PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') - # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field @@ -187,6 +182,29 @@ if DEBUG: "rest_framework.renderers.BrowsableAPIRenderer" ) +# aws +AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME', None) +AWS_S3_FILE_OVERWRITE = True +AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com' +AWS_S3_OBJECT_PARAMETERS = { + 'CacheControl': 'max-age=86400', +} +AWS_DEFAULT_REGION = os.environ.get( + 'AWS_DEFAULT_REGION', + 'us-east-1' +) +AWS_LOCATION = 'static' + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +if AWS_STORAGE_BUCKET_NAME: + STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/' + STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' +else: + STATIC_URL = '/static/' + PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) + STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') # allauth ACCOUNT_EMAIL_VERIFICATION = "none" diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index a45acfc..4e6d65b 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -18,6 +18,7 @@ import Project from "./components/Project"; import Profile from "./components/Profile"; import Signup from "./components/Auth/Signup"; import Login from "./components/Auth/Login"; +import GitHub from "./components/Auth/GitHub"; import { ProtectedRouteProps } from "./partials/ProtectedRoute"; import ProtectedRoute from "./partials/ProtectedRoute"; @@ -131,6 +132,7 @@ export default function App() { } /> } /> + } /> diff --git a/services/frontend/src/components/Auth/GitHub/LoginBtn.tsx b/services/frontend/src/components/Auth/GitHub/LoginBtn.tsx new file mode 100644 index 0000000..cc1fd99 --- /dev/null +++ b/services/frontend/src/components/Auth/GitHub/LoginBtn.tsx @@ -0,0 +1,33 @@ +import { + REACT_APP_GITHUB_CLIENT_ID, + REACT_APP_GITHUB_SCOPE +} from "../../../constants"; + +const LoginBtn = () => { + return ( +
+ +
+ +
+ GitHub +
+
+ ); +}; + +export default LoginBtn; diff --git a/services/frontend/src/components/Auth/GitHub/index.tsx b/services/frontend/src/components/Auth/GitHub/index.tsx new file mode 100644 index 0000000..5e0562b --- /dev/null +++ b/services/frontend/src/components/Auth/GitHub/index.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { LOCAL_STORAGE } from "../../../constants"; +import { toaster } from "../../../utils"; +import { socialAuth } from "../../../hooks/useSocialAuth"; +import { authLoginSuccess } from "../../../reducers"; +import Spinner from "../../global/Spinner"; + +interface IGitHubProps { + dispatch: any; +} + +const GitHub = (props: IGitHubProps) => { + const navigate = useNavigate(); + const { dispatch } = props; + const [searchParams] = useSearchParams(); + const [loading, setLoading] = useState(false); + const code = searchParams.get("code"); + + useEffect(() => { + if (code) { + setLoading(true); + socialAuth(code) + .then((data: any) => { + localStorage.setItem( + LOCAL_STORAGE, + JSON.stringify({ + access_token: data.access_token, + refresh_token: data.refresh_token + }) + ); + dispatch(authLoginSuccess(data)); + navigate("/"); + }) + .catch(() => { + localStorage.removeItem(LOCAL_STORAGE); + navigate(`/login`); + toaster(`Something went wrong! Session may have expired.`, "error"); + }) + .finally(() => { + setLoading(false); + }); + } else { + navigate(`/login`); + } + }, [code]); + + return ( +
+
+ {loading && ( +
+ + logging in... +
+ )} +
+
+ ); +}; + +export default GitHub; diff --git a/services/frontend/src/components/Auth/Login/index.tsx b/services/frontend/src/components/Auth/Login/index.tsx index dc75020..45adf81 100644 --- a/services/frontend/src/components/Auth/Login/index.tsx +++ b/services/frontend/src/components/Auth/Login/index.tsx @@ -1,12 +1,17 @@ import { useState } from "react"; import { useFormik } from "formik"; import { Link, useNavigate } from "react-router-dom"; +import { + REACT_APP_GITHUB_CLIENT_ID, + REACT_APP_GITHUB_SCOPE +} from "../../../constants"; import Spinner from "../../../components/global/Spinner"; import { toaster } from "../../../utils"; import { checkHttpStatus } from "../../../services/helpers"; import { logIn } from "../../../services/auth"; import { LOCAL_STORAGE } from "../../../constants"; import { authLoginSuccess } from "../../../reducers"; +import LoginBtn from "../GitHub/LoginBtn"; interface IProfileProps { dispatch: any; @@ -62,7 +67,7 @@ const Login = (props: IProfileProps) => { <>
-

+

Sign in

@@ -152,6 +157,19 @@ const Login = (props: IProfileProps) => { Create account
+ +
+
+
+ + Or login with + +
+
+ {REACT_APP_GITHUB_SCOPE && REACT_APP_GITHUB_CLIENT_ID && ( + + )} +
diff --git a/services/frontend/src/components/Auth/Signup/index.tsx b/services/frontend/src/components/Auth/Signup/index.tsx index 5267886..21796fd 100644 --- a/services/frontend/src/components/Auth/Signup/index.tsx +++ b/services/frontend/src/components/Auth/Signup/index.tsx @@ -5,8 +5,13 @@ import Spinner from "../../../components/global/Spinner"; import { toaster } from "../../../utils"; import { checkHttpStatus } from "../../../services/helpers"; import { signup } from "../../../services/auth"; -import { LOCAL_STORAGE } from "../../../constants"; +import { + LOCAL_STORAGE, + REACT_APP_GITHUB_CLIENT_ID, + REACT_APP_GITHUB_SCOPE +} from "../../../constants"; import { authLoginSuccess } from "../../../reducers"; +import LoginBtn from "../GitHub/LoginBtn"; interface IProfileProps { dispatch: any; @@ -220,6 +225,19 @@ const Signup = (props: IProfileProps) => { Already have an account? + +
+
+
+ + Or signup with + +
+
+ {REACT_APP_GITHUB_SCOPE && REACT_APP_GITHUB_CLIENT_ID && ( + + )} +
diff --git a/services/frontend/src/components/Modal/import/index.tsx b/services/frontend/src/components/Modal/import/index.tsx index 97ecf9e..0e959ae 100644 --- a/services/frontend/src/components/Modal/import/index.tsx +++ b/services/frontend/src/components/Modal/import/index.tsx @@ -1,5 +1,5 @@ -import { useCallback, useMemo, useState } from "react"; -import { Field, Formik } from "formik"; +import { useCallback, useMemo } from "react"; +import { Formik } from "formik"; import { styled } from "@mui/joy"; import { XIcon } from "@heroicons/react/outline"; import { CallbackFunction } from "../../../types"; @@ -13,7 +13,6 @@ import TextField from "../../global/FormElements/TextField"; import { toaster } from "../../../utils"; import { reportErrorsAndSubmit } from "../../../utils/forms"; import { ScrollView } from "../../ScrollView"; -import lodash from "lodash"; interface IModalImportProps { onHide: CallbackFunction; diff --git a/services/frontend/src/constants/index.ts b/services/frontend/src/constants/index.ts index c3be7e5..0ca9c77 100644 --- a/services/frontend/src/constants/index.ts +++ b/services/frontend/src/constants/index.ts @@ -1,3 +1,6 @@ export const API_SERVER_URL = process.env.REACT_APP_API_SERVER; +export const REACT_APP_GITHUB_CLIENT_ID = + process.env.REACT_APP_GITHUB_CLIENT_ID; +export const REACT_APP_GITHUB_SCOPE = process.env.REACT_APP_GITHUB_SCOPE; export const PROJECTS_FETCH_LIMIT = 300; export const LOCAL_STORAGE = "CtkLocalStorage"; diff --git a/services/frontend/src/hooks/useSocialAuth.ts b/services/frontend/src/hooks/useSocialAuth.ts new file mode 100644 index 0000000..7739bae --- /dev/null +++ b/services/frontend/src/hooks/useSocialAuth.ts @@ -0,0 +1,16 @@ +import axios from "axios"; +import { API_SERVER_URL } from "../constants"; + +export const socialAuth = async (code: string) => { + const response = await axios({ + method: "post", + url: `${API_SERVER_URL}/auth/github/`, + data: { + code: code + }, + headers: { + "Content-Type": "application/json" + } + }); + return response.data; +};