Skip to content

Commit 928c409

Browse files
committed
initial commit
0 parents  commit 928c409

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+12031
-0
lines changed

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
venv
2+

Diff for: README.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Google Oauth Django Angular
2+
3+
This repository contains the most minimum setup needed to Authenticate via API using Django + REST Framework + Angular on the frontend.
4+
5+
## Run the repository
6+
7+
- Download/Clone the repository
8+
- Run `cd backend && pip install -r requirements`
9+
- Run `cd frontend && npm install`
10+
- Create a `environment.development.ts` file in the `src/environments/` folder and update with your Google ID
11+
12+
```
13+
// environment.development.ts
14+
export const environment = {
15+
google_id: '<YOUR_GOOGLE_ID>.apps.googleusercontent.com'
16+
};
17+
```
18+
19+
- Create a `.env` file in the root of your django project (in this case, `backend` and update with your Google ID)
20+
21+
```
22+
# .env
23+
GOOGLE_OAUTH2_CLIENT_ID='YOUR_GOOGLE_ID.apps.googleusercontent.com'
24+
GOOGLE_OAUTH2_CLIENT_SECRET='YOUR_GOOGLE_SECRET'
25+
```
26+
27+
- Run both the django and angular servers
28+
29+
```
30+
cd backend && python manage.py runserver
31+
cd frontend && ng serve
32+
```
33+
34+
- Visit `http://localhost:4200` for the frontend, and visit `http://localhost:8000` for the django backend

Diff for: backend/.gitignore

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Created by https://www.gitignore.io
2+
3+
### OSX ###
4+
.DS_Store
5+
.AppleDouble
6+
.LSOverride
7+
8+
# Icon must end with two \r
9+
Icon
10+
11+
12+
# Thumbnails
13+
._*
14+
15+
# Files that might appear on external disk
16+
.Spotlight-V100
17+
.Trashes
18+
19+
# Directories potentially created on remote AFP share
20+
.AppleDB
21+
.AppleDesktop
22+
Network Trash Folder
23+
Temporary Items
24+
.apdisk
25+
26+
27+
### Python ###
28+
# Byte-compiled / optimized / DLL files
29+
__pycache__/
30+
*.py[cod]
31+
32+
# C extensions
33+
*.so
34+
35+
# Distribution / packaging
36+
.Python
37+
env/
38+
build/
39+
develop-eggs/
40+
dist/
41+
downloads/
42+
eggs/
43+
lib/
44+
lib64/
45+
parts/
46+
sdist/
47+
var/
48+
*.egg-info/
49+
.installed.cfg
50+
*.egg
51+
52+
# PyInstaller
53+
# Usually these files are written by a python script from a template
54+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
55+
*.manifest
56+
*.spec
57+
58+
# Installer logs
59+
pip-log.txt
60+
pip-delete-this-directory.txt
61+
62+
# Unit test / coverage reports
63+
htmlcov/
64+
.tox/
65+
.coverage
66+
.cache
67+
nosetests.xml
68+
coverage.xml
69+
70+
# Translations
71+
*.mo
72+
*.pot
73+
74+
# Sphinx documentation
75+
docs/_build/
76+
77+
# PyBuilder
78+
target/
79+
80+
81+
### Django ###
82+
*.log
83+
*.pot
84+
*.pyc
85+
__pycache__/
86+
local_settings.py
87+
88+
.env
89+
db.sqlite3

Diff for: backend/main/__init__.py

Whitespace-only changes.

Diff for: backend/main/admin.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

Diff for: backend/main/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class MainConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'main'

Diff for: backend/main/migrations/0001_initial.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 5.0.4 on 2024-04-25 11:09
2+
3+
import django.contrib.auth.models
4+
import django.contrib.auth.validators
5+
import django.utils.timezone
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
('auth', '0012_alter_user_first_name_max_length'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='User',
20+
fields=[
21+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('password', models.CharField(max_length=128, verbose_name='password')),
23+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
24+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
25+
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
26+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
27+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
28+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
29+
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
30+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
31+
('email', models.CharField(max_length=250, unique=True)),
32+
('registration_method', models.CharField(choices=[('email', 'Email'), ('google', 'Google')], default='email', max_length=10)),
33+
('photo', models.URLField(blank=True, default='')),
34+
('country', models.CharField(blank=True, default='', max_length=100)),
35+
('about', models.TextField(blank=True, default='')),
36+
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
37+
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
38+
],
39+
options={
40+
'verbose_name': 'user',
41+
'verbose_name_plural': 'users',
42+
'abstract': False,
43+
},
44+
managers=[
45+
('objects', django.contrib.auth.models.UserManager()),
46+
],
47+
),
48+
]

Diff for: backend/main/migrations/__init__.py

Whitespace-only changes.

Diff for: backend/main/models.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.contrib.auth.models import AbstractUser
2+
from django.db import models
3+
4+
REGISTRATION_CHOICES = [
5+
('email', 'Email'),
6+
('google', 'Google'),
7+
]
8+
9+
10+
class User(AbstractUser):
11+
email = models.CharField(
12+
max_length=250, unique=True, null=False, blank=False)
13+
registration_method = models.CharField(
14+
max_length=10, choices=REGISTRATION_CHOICES, default='email')
15+
photo = models.URLField(blank=True, default='')
16+
country = models.CharField(max_length=100, default='', blank=True)
17+
about = models.TextField(blank=True, default='')
18+
19+
def __str__(self):
20+
return self.username

Diff for: backend/main/serializers.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from rest_framework import serializers
2+
from .models import User
3+
4+
5+
class UserSerializer(serializers.ModelSerializer):
6+
7+
class Meta:
8+
model = User
9+
fields = ['username', 'first_name', 'last_name',
10+
'email', 'country', 'about', 'photo']

Diff for: backend/main/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.test import TestCase
2+
3+
# Create your tests here.

Diff for: backend/main/urls.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.urls import path
2+
from main.views import GoogleLoginApi
3+
4+
urlpatterns = [
5+
path("login/google/", GoogleLoginApi.as_view(),
6+
name="login-with-google"),
7+
]

Diff for: backend/main/views.py

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import os
2+
from rest_framework.views import APIView
3+
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
4+
from rest_framework.response import Response
5+
from .models import User
6+
from .serializers import UserSerializer
7+
from google.oauth2 import id_token
8+
from google.auth.transport import requests
9+
from rest_framework import status
10+
import django.utils.timezone as timezone
11+
from rest_framework.permissions import AllowAny, IsAuthenticated
12+
from django.contrib.auth import get_user_model
13+
from .serializers import UserSerializer
14+
15+
16+
CLIENT_ID = os.environ.get('GOOGLE_OAUTH2_CLIENT_ID')
17+
18+
19+
def generate_tokens_for_user(user):
20+
"""
21+
Generate access and refresh tokens for the given user
22+
"""
23+
serializer = TokenObtainPairSerializer()
24+
token_data = serializer.get_token(user)
25+
access_token = token_data.access_token
26+
refresh_token = token_data
27+
return access_token, refresh_token
28+
29+
30+
class GoogleLoginApi(APIView):
31+
permission_classes = [AllowAny]
32+
33+
def get(request, *args, **kwargs):
34+
return Response({
35+
'message': 'Response'
36+
}, status=status.HTTP_200_OK)
37+
38+
def post(self, request, *args, **kwargs):
39+
google_jwt = request.data.get('jwt')
40+
try:
41+
# Specify the CLIENT_ID of the app that accesses the backend:
42+
idinfo = id_token.verify_oauth2_token(
43+
google_jwt, requests.Request(), CLIENT_ID)
44+
45+
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
46+
raise ValueError('Wrong issuer.')
47+
48+
# ID token is valid. Get the user's Google Account ID from the decoded token.
49+
user_id = idinfo['sub']
50+
user_email = idinfo['email']
51+
# Verify that the access token is valid for this app.
52+
if idinfo['aud'] != CLIENT_ID:
53+
raise ValueError('Wrong client ID.')
54+
55+
# Check if user exists in the database
56+
try:
57+
user = User.objects.get(email=user_email)
58+
user.last_login = timezone.now()
59+
user.save()
60+
# Generate access and refresh tokens for the user
61+
access_token, refresh_token = generate_tokens_for_user(user)
62+
response_data = {
63+
'user': UserSerializer(user).data,
64+
'access_token': str(access_token),
65+
'refresh_token': str(refresh_token)
66+
}
67+
return Response(response_data)
68+
except User.DoesNotExist:
69+
username = user_email.split('@')[0]
70+
first_name = idinfo.get('given_name', '')
71+
last_name = idinfo.get('family_name', '')
72+
photo = idinfo.get('photoUrl', '')
73+
74+
user = User.objects.create(
75+
username=username,
76+
email=user_email,
77+
first_name=first_name,
78+
last_name=last_name,
79+
registration_method='google',
80+
photo=photo,
81+
)
82+
83+
access_token, refresh_token = generate_tokens_for_user(user)
84+
response_data = {
85+
'user': UserSerializer(user).data,
86+
'access_token': str(access_token),
87+
'refresh_token': str(refresh_token)
88+
}
89+
return Response(response_data)
90+
91+
except ValueError:
92+
return Response({
93+
'error': 'Invalid token'
94+
}, status=status.HTTP_400_BAD_REQUEST)

Diff for: backend/manage.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env python
2+
"""Django's command-line utility for administrative tasks."""
3+
import os
4+
import sys
5+
6+
7+
def main():
8+
"""Run administrative tasks."""
9+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
10+
try:
11+
from django.core.management import execute_from_command_line
12+
except ImportError as exc:
13+
raise ImportError(
14+
"Couldn't import Django. Are you sure it's installed and "
15+
"available on your PYTHONPATH environment variable? Did you "
16+
"forget to activate a virtual environment?"
17+
) from exc
18+
execute_from_command_line(sys.argv)
19+
20+
21+
if __name__ == '__main__':
22+
main()

Diff for: backend/project/__init__.py

Whitespace-only changes.

Diff for: backend/project/asgi.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
ASGI config for project project.
3+
4+
It exposes the ASGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
8+
"""
9+
10+
import os
11+
12+
from django.core.asgi import get_asgi_application
13+
14+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
15+
16+
application = get_asgi_application()

0 commit comments

Comments
 (0)