---
sidebar_label: Tutorial 3
sidebar_position: 5
Path: docs/tutorial-3
---
# Tutorial 3: Authentication, Session, Cookies, and Selenium
Platform-Based Programming (CSGE602022) — Organized by the Faculty of Computer Science Universitas Indonesia, Odd Semester 2025/2026
---
## Learning Objectives
After completing this tutorial, students are expected to be able to:
- Understand the basic concepts of authentication in web development.
- Understand the role and function of cookies and sessions in web development.
- Understand how cookies and sessions work in web development.
- Implement cookies and sessions in a web project.
- Gain an introduction to using Selenium.
## Introduction to HTTP
HTTP (_HyperText Transfer Protocol_) is a protocol used for communication between a _client_ and a _server_. HTTP is _stateless_ which means that each transaction or activity is treated as a completely **new** one with no previous data being **stored** for the current transaction or activity.
Some basic concepts about HTTP:
1. **_Client/Server_**: Interaction occurs between the _client/server_. The client is the party that makes the _request_, while the server is the party that provides the *response*.
2. **_Stateless_**: Each activity (_request/response_) is independent and is not stored or linked to previous activities.
3. **_OSI Layer/Model_**: The *Open Systems Interconnection (OSI)* model describes seven layers that computer systems use to communicate over a network. The 7-layer OSI model consists of the _Application Layer_, _Presentation Layer_, _Session Layer_, _Transport Layer_, _Network Layer_, _Data Link Layer_, and _Physical Layer_.
4. **_Application Layer_**: In the OSI Model mentioned above, websites operate at the _Application Layer_. Meanwhile, the _request/response_ process occurs at the _Transport Layer_, which generally uses the TCP protocol to determine how data is transmitted. The _Application Layer_ does not concern itself with what the _Transport Layer_ does (how data is sent, processed, etc.) because the _Application Layer_ focuses only on the _request_ and _response_.
:::info
The other OSI layers will be covered in the Computer Networks/Data Communication Networks course. You can also look them up on your own if you’re curious. 😉
:::
5. **_Client Actions Method_**: There are several methods used by the _client_ when making a _request_. Examples: GET, POST, PUT, DELETE, etc. A more detailed explanation can be found [here](https://www.restapitutorial.com/lessons/httpmethods.html).
6. **_Server Status Code_**: These are status codes provided by the server in response to a *request* on a web page. Examples: 200 (OK), 404 (_Page Not Found_), 500 (_Internal Server Error_), etc. A more detailed explanation can be found [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
7. **_Headers_**: These are small pieces of information sent along with a _request_ and _response_. This information is useful as additional data to process the _request/response_. Example: in the _headers_, there is `content-type: json`. This means the content type being requested or sent is `json`. _Headers_ also store _cookies_.
## Introduction to Cookies & Session
All communication between the client and the server is carried out through the HTTP protocol, where HTTP is a _stateless protocol_. This means that each state is independent and unrelated to the others. As a result, the client computer running the browser needs to establish a TCP connection to the server **every time a request is made**.
Without a persistent connection between the client and server, the _software_ on either side (_endpoints_) cannot rely solely on the TCP connection for _holding state_ or _holding session state_.
### What is a holding state?
As an example, suppose you want to access page A on a website that requires you to be logged in. You log in to the website and successfully open page A. Later, when you want to move to page B on the same website, without a process of _holding state_, **you would be asked to log in again**. This would happen every time you access a different page, even though you are still on the same website.
The process of telling the server “who” is currently logged in and storing that data is a form of dialogue between the client and the server. This process forms the basis of a _session_ — _a semi-permanent exchange of information_. Since it is difficult for HTTP to maintain state (because HTTP is a _stateless protocol_), a technique is needed to solve this problem. The techniques used are called _Cookies_ and _Sessions_.
### How to implement a _holding state_?
One of the most common methods for _holding state_ is by using a _session ID_ stored as a _cookie_ on the client’s computer. A _session ID_ can be considered of as a _token_ (a sequence of characters) that identifies a unique _session_ for a particular web application. Instead of storing all kinds of information as _cookies_ on the client, such as _username_, name, and password, only the _session ID_ is stored.
This _session ID_ can then be mapped to a data structure on the web server. In that data structure, you can store all the information you need. This approach is much safer for keeping user information than storing it directly in a cookie, since the information cannot be tampered with by the client or intercepted by suspicious connections.
Another advantage of this approach is that it is more “appropriate” when a lot of data needs to be stored, because a cookie can only hold up to 4 KB of data. Imagine you log in to a website or application and receive a session ID (a session identifier). To achieve _holding state_ in HTTP, which is stateless, the browser usually sends the session ID to the server with every request. That way, whenever a request comes in, the server can react with something like, “Oh, this is the right person!” The server then looks up the corresponding state information in its memory or in a database based on the received session ID, and returns the requested data.
An important difference to remember is that cookie data is stored on the client side, while session data is usually stored on the server side. For a more detailed discussion of _stateless, stateful, cookie_, and _session_, you can read [here](https://sethuramanmurali.wordpress.com/2013/07/07/stateful-stateless-cookie-and-session/).
Here is a brief table that explains the differences between _cookies_, _session_, and _local storage_.
| | **_Cookies_** | **_Local Storage_** | **_Sessions_** |
| --------------------- | -------------- | ------------------- | ------------------ |
| **Capacity** | 4 KB | 5 MB | 5 MB |
| **Browser Technology** | HTML4/HTML5 | HTML5 | HTML5 |
| **Accessibility** | All windows | All windows | Same tab |
| **Expiration** | Manually set | Permanent | When tab is closed |
## Pre-Tutorial Notes
Before you begin, and to help you follow Tutorial 3 smoothly, we expect the following results from Tutorial 2:
- The directory structure of `football_news` on your local machine should look like this:
```
football_news
├── env
├── football_news
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── main
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0001_initial.py
│ ├── templates
│ │ ├── create_news.html
│ │ └── main.html
│ │ └── news_detail.html
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── templates
│ └── base.html
├── .gitignore
├── manage.py
└── requirements.txt
```
For more details, the `football_news` repository structure should look like this:

## Tutorial: Creating a Registration Function and Form
In the previous tutorial, we created a form to add a football news entry. How was it? Pretty easy, right? In this tutorial, we will make the main (`main`) page restricted by requiring users to create an account. This way, any user who wants to access the `main` page must log in first to gain access.
1. First, activate the *virtual environment* in your terminal. (**Hint: Remember Tutorial 0!**)
2. Open `views.py` inside the `main` subdirectory of your project. Add the following imports `UserCreationForm` and `messages` at the very top:
```python
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages
```
**Code Explanation:**
`UserCreationForm` is a built-in form import that makes it easier to create a user registration form in your web application. With this form, new users can register easily on your site without having to write all the code from scratch.
3. Add the `register` function below into `views.py`. This function automatically generates a registration form and creates a new user account when the data is *submitted* from the form.
```python
def register(request):
form = UserCreationForm()
if request.method == "POST":
form = UserCreationForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Your account has been successfully created!')
return redirect('main:login')
context = {'form':form}
return render(request, 'register.html', context)
```
**Code Explanation:**
* `form = UserCreationForm(request.POST)` is used to create a new `UserCreationForm` (imported earlier) by passing in the `QueryDict` based on the user input from `request.POST`.
* `form.is_valid()` is used to validate the input data from the *form*.
* `form.save()` is used to create and save the data from the *form*.
* `messages.success(request, 'Your account has been successfully created!')` is used to display a message to the user after an action is performed.
* `return redirect('main:show_main')` is used to perform a *redirect* after the form data has been successfully saved.
4. Create a new HTML file named `register.html` inside the `main/templates` directory. The content of `register.html` can be filled with the following template:
```html
{% extends 'base.html' %}
{% block meta %}
<title>Register</title>
{% endblock meta %}
{% block content %}
<div class="login">
<h1>Register</h1>
<form method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
<tr>
<td></td>
<td><input type="submit" name="submit" value="Daftar" /></td>
</tr>
</table>
</form>
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock content %}
```
::: tip
We use the tag `{{ form.as_table }}` to easily render the form in a table format. For more information, you can read about it [here](https://docs.djangoproject.com/en/5.1/topics/forms/#reusable-form-templates).
:::
5. Open `urls.py` in the `main` subdirectory and import the function you just created:
```python
from main.views import register
```
6. Add a `path` entry into `urlpatterns` to make the imported function accessible:
```python
urlpatterns = [
...
path('register/', register, name='register'),
]
```
Now we have added a user registration form and created the `register` mechanism. Next, we will create a *login* form so that users can authenticate their accounts.
## Tutorial: Creating a Login Function
1. Reopen `views.py` inside the `main` subdirectory. Add the *imports* for `authenticate`, `login`, and `AuthenticationForm` at the top.
```python
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth import authenticate, login
```
**Code Explanation:**
In short, the `authenticate` and `login` functions imported above are built-in Django functions used to perform authentication and login (if authentication succeeds).
You can read more about this in the [Django Authentication Docs](https://docs.djangoproject.com/en/4.2/topics/auth/default/).
2. Add the `login_user` function below into `views.py`. This function is used to authenticate users who want to log in.
```python
def login_user(request):
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
return redirect('main:show_main')
else:
form = AuthenticationForm(request)
context = {'form': form}
return render(request, 'login.html', context)
```
**Code Explanation:**
* `if request.method == 'POST'`: Checks whether the user submitted the login form. If yes, the form must be validated first before logging into the Django system.
* `login(request, user)`: Performs the login process. If the user is valid, this function will create a *session* for the logged-in user.
* The `else:` block is executed when the user first accesses the login page. Django creates an `AuthenticationForm` object based on the user’s request and renders it in the template through the context.
3. Create a new HTML file named `login.html` inside the `main/templates` directory. You can use the following template:
```html
{% extends 'base.html' %}
{% block meta %}
<title>Login</title>
{% endblock meta %}
{% block content %}
<div class="login">
<h1>Login</h1>
<form method="POST" action="">
{% csrf_token %}
<table>
{{ form.as_table }}
<tr>
<td></td>
<td><input class="btn login_btn" type="submit" value="Login" /></td>
</tr>
</table>
</form>
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %} Don't have an account yet?
<a href="{% url 'main:register' %}">Register Now</a>
</div>
{% endblock content %}
```
4. Open `urls.py` inside the `main` subdirectory and import the function you just created.
```python
from main.views import login_user
```
5. Add a *path url* inside the `urlpatterns` so that you can access the function you just imported.
```python
urlpatterns = [
...
path('login/', login_user, name='login'),
]
```
Now we have added a login form and created the _login_ mechanism. Next, we will implement a _logout_ mechanism and add a _logout_ button on the `main` page.
## Tutorial: Creating a Logout Function
1. Reopen `views.py` inside the `main` subdirectory. Add the `logout` import at the top, together with `authenticate` and `login`.
```python
from django.contrib.auth import authenticate, login, logout
```
2. Add the following function to `views.py`. This function handles the logout mechanism.
```python
def logout_user(request):
logout(request)
return redirect('main:login')
```
**Code Explanation:**
- `logout(request)` is used to remove the session of the currently logged-in user.
- `return redirect('main:login')` redirects the user back to the login page in your Django app.
3. Open `main.html` inside the `main/templates` directory and add the following snippet **after** the hyperlink tag for _Add News_.
```html
...
<a href="{% url 'main:logout' %}">
<button>Logout</button>
</a>
...
```
**Code Explanation:**
- `{% url 'main:logout' %}` dynamically generates the URL based on the `app_name` and the `name` defined in `urls.py`.
In general, the syntax is `{% url 'app_name:view_name' %}`:
- `app_name` is the name of the app defined in `urls.py`. If you have specified the `app_name` attribute in `urls.py`, Django uses it to refer to that app. Otherwise, it defaults to the folder name of your app.
- `view_name` is the name of the URL you want to reference, defined in the `name` parameter of the `path()` function in `urls.py`. **Here is how `urls.py` looks. For example, you can dynamically call the `login` view URL using the syntax `{% url 'main:login' %}`.**

4. Open `urls.py` inside the `main` subdirectory and import the logout function you just created.
```python
from main.views import logout_user
```
5. Add a *path url* to the `urlpatterns` so the logout function can be accessed.
```python
urlpatterns = [
...
path('logout/', logout_user, name='logout'),
]
```
Now you’ve created the `logout` mechanism and completed the authentication system for this project.
## Tutorial: Restricting Access to the Main Page
:::info
Restricting access to the main page means limiting who can open that page. For example, only logged-in users or admins.
:::
1. Reopen `views.py` in the `main` subdirectory. Add the import for `login_required` at the top.
```python
from django.contrib.auth.decorators import login_required
```
**Code Explanation:**
- This line imports the `login_required` decorator from Django’s authentication system.
- Decorators allow us to add functionality to a function without modifying its code.
- You can read more about decorators [here](https://www.geeksforgeeks.org/python/decorators-in-python/).
2. Add the line `@login_required(login_url='/login')` above the `show_main` function to apply the decorator you just imported.
```python
...
@login_required(login_url='/login')
def show_main(request):
...
```
**Code Explanation:**
- This line applies the `login_required` decorator to the `show_main` function, so the main page can only be accessed by authenticated (logged-in) users.
- For more details, see the [Django documentation on `login_required`](https://docs.djangoproject.com/en/5.2/topics/auth/default/#the-login-required-decorator).
After adding the access restriction to the main page, run the Django project with `python manage.py runserver` and open `http://localhost:8000/` in your preferred web browser. If the user is **logged out**, they should be redirected to the **login page** instead of seeing the main page.
## Tutorial: Using Data from *Cookies*
1. First, **logout** if you are currently running the Django application.
2. Reopen `views.py` in the `main` subdirectory. Add the imports for `HttpResponseRedirect`, `reverse`, and `datetime` at the top:
```python
import datetime
from django.http import HttpResponseRedirect
from django.urls import reverse
```
3. Update the `login_user` function to save a new cookie named `last_login` containing the timestamp of the user’s last login. Modify the code inside the `if form.is_valid()` block as follows:
```python
...
if form.is_valid():
user = form.get_user()
login(request, user)
response = HttpResponseRedirect(reverse("main:show_main"))
response.set_cookie('last_login', str(datetime.datetime.now()))
return response
...
```
:::warning
Pay attention to the modified code indentation!
:::
**Code Explanation:**
- `login(request, user)` logs in the user using Django’s authentication system.
- `response = HttpResponseRedirect(reverse("main:show_main"))` sets a redirect to the main page after the response.
- `response.set_cookie('last_login', str(datetime.datetime.now()))` registers the `last_login` cookie in the response with the current timestamp.
4. In the `show_main` function, add `'last_login': request.COOKIES['last_login']` to the `context` variable. For example:
```python
context = {
'npm' : '240123456',
'name': 'Haru Urara',
'class': 'PBP A',
'news_list': news_list
'last_login': request.COOKIES['last_login']
}
```
**Code Explanation:**
- We access the registered cookie in `request` using `request.COOKIES['last_login']`.
- The user’s last login time can now be displayed on the web page by accessing the `last_login` key.
5. Update the `logout_user` function to delete the `last_login` cookie after logout:
```python
def logout_user(request):
logout(request)
response = HttpResponseRedirect(reverse('main:login'))
response.delete_cookie('last_login')
return response
```
**Code Explanation:**
- `response.delete_cookie('last_login')` removes the `last_login` cookie from the response.
6. Open `main.html` in the `main/templates` directory and add the following code **after the logout button** to display the user’s last login time:
```html
...
<h5>Last login session: {{ last_login }}</h5>
...
```
7. **Refresh** the login page (or run your Django project with `python manage.py runserver` if it’s not running) and try logging in. Your `last_login` data will appear on the main page.
8. If you are using a Chromium-based browser like Google Chrome or Microsoft Edge, you can view the `last_login` cookie in Developer Tools (<kbd>Ctrl + Shift + I</kbd> or <kbd>Cmd + Option + I</kbd>) under the **Application** tab:
- Click on **Cookies** under the **Storage** group to see all available cookies. Besides `last_login`, you will also see `sessionid` and `csrftoken`. Example:

9. When you **logout** and check the cookie history, the `last_login` cookie created earlier will disappear and be created again when you log in next time.
::: info
Before moving on to the next tutorial, try to **create at least one account** in your Django project.
:::
## Tutorial: Connecting the `News` Model with `User`
:::danger
🔥 **DANGER**
You need to follow the tutorials in order from the beginning before executing the steps below. If you do not follow the tutorials sequentially, we are not responsible for errors outside the tutorial content that may arise from the following steps.
:::
Lastly, we will link each `News` object with the user who created it. This way, each logged-in user will only be able to see the news they created themselves.
Follow these steps:
1. Open the `models.py` file in the `main` subdirectory, then add the following import along with the existing ones:
```python
...
from django.contrib.auth.models import User
...
```
2. In the `News` model you previously created, add the following code:
```python
class News(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
...
```
**Code Explanation:**
This code links each `News` object to a `User` via a relationship, ensuring that every news entry is associated with a specific user.
:::success
💡 **TIPS**
You will learn more about `ForeignKey` in Database courses. Further explanation of Django’s `ForeignKey` can be read [here](https://docs.djangoproject.com/en/4.2/topics/db/examples/many_to_one/).
:::
3. Reopen `views.py` in the `main` subdirectory and modify the `create_news` function as follows:
```python
def create_news(request):
form = NewsForm(request.POST or None)
if form.is_valid() and request.method == 'POST':
news_entry = form.save(commit = False)
news_entry.user = request.user
news_entry.save()
return redirect('main:show_main')
context = {
'form': form
}
return render(request, "create_news.html", context)
...
```
**Code Explanation:**
The `commit=False` parameter prevents Django from immediately saving the form object to the database, giving us the chance to modify it first.
Here, we set the `user` field to `request.user`, i.e., the logged-in user. This ensures that every created object is automatically linked to its creator.
4. Modify the `show_main` function so that it looks like this:
```python
...
@login_required(login_url='/login')
def show_main(request):
filter_type = request.GET.get("filter", "my") # default 'my'
if filter_type == "all":
news_list = News.objects.all()
else:
news_list = News.objects.filter(user=request.user)
context = {
'npm': '240123456',
'name': request.user.username,
'class': 'PBP A',
'news_list': news_list,
'last_login': request.COOKIES['last_login'],
}
return render(request, "main.html",context)
...
```
**Code Explanation:**
`show_main` displays the main page after the user logs in and includes **filtering articles** by author. The `filter` query parameter in the URL has two options: `"my"` shows only articles written by the logged-in user, while `"all"` shows all articles. Other than that, the user information, such as `name`, is taken directly from the **username** of the logged-in user.
5. Add filter buttons (My Articles / All Articles) to the `main.html` page:
```html
{% extends 'base.html' %}
{% block content %}
<h1>Football News</h1>
<h5>NPM: </h5>
<p>{{ npm }}</p>
<h5>Name:</h5>
<p>{{ name }}</p>
<h5>Class:</h5>
<p>{{ class }}</p>
<!-- Add this code -->
<a href="?filter=my">
<button type="button">My Articles</button>
</a>
<a href="?filter=all">
<button type="button">All Articles</button>
</a>
...
```

6. Show the author’s name in `news_detail.html`:
```html
{% extends 'base.html' %}
{% block content %}
<p><a href="{% url 'main:show_main' %}"><button>← Back to News List</button></a></p>
<h1>{{ news.title }}</h1>
<p><b>{{ news.get_category_display }}</b>{% if news.is_featured %} |
<b>Featured</b>{% endif %}{% if news.is_news_hot %} |
<b>Hot</b>{% endif %} | <i>{{ news.created_at|date:"d M Y, H:i" }}</i>
| Views: {{ news.news_views }}</p>
{% if news.thumbnail %}
<img src="{{ news.thumbnail }}" alt="News thumbnail" width="300">
<br /><br />
{% endif %}
<p>{{ news.content }}</p>
<!-- Add this code -->
<p>Author: {{ news.user.username }}</p>
{% endblock content %}
```

The author reflects the creator of the article, not the logged-in user. Try using two different accounts to verify.
:::danger
🔥 **DANGER**
Before executing the next steps, make sure the database contains at least one user. This is necessary because the next step will require setting a **default value** for the _user_ field in existing rows. Migration will fail if no user exists.
:::
7. Save all changes and run `python manage.py makemigrations`.
8. You will likely see an error during migration. Choose `1` to set a default value for the `user` field in all existing rows.

9. Enter `1` again to assign the user with ID 1 (created earlier) to existing model rows.

10. Run `python manage.py migrate` to apply the migrations.
:::success
💡 **Reminder**
Every time you modify a model, such as adding or changing attributes, you **must run migrations** to reflect those changes.
:::
11. Finally, prepare your web app for a **production environment**. Update the `DEBUG` variable in `settings.py` as follows:
```python
PRODUCTION = os.getenv("PRODUCTION", False)
DEBUG = not PRODUCTION
```
**Why this matters:**
If `DEBUG = True` in production, Django will display detailed error pages containing sensitive information (e.g., database passwords, file paths, environment variables). This can be dangerous if accessed by unauthorized users.
Run your Django project with `python manage.py runserver` and visit [http://localhost:8000/](http://localhost:8000/) in your preferred browser. Try creating a new account and logging in. Observe that the main page does **not** display news created by other users. This confirms that you have successfully linked `News` objects with the `User` who created them.
## Tutorial: Introduction to Selenium (OPTIONAL)
Selenium is a free tool used to automate web browsers. With Selenium, we can write programs that **control the browser as if it were being used by a human**, such as clicking buttons, filling forms, navigating pages, and extracting data from a website. The advantage of Selenium is that it works across major browsers like Chrome, Firefox, Edge, and Safari without significant code changes, since it follows the official W3C WebDriver standard.
### What is a WebDriver?
WebDriver is the main component in Selenium that acts as a **“bridge”** between the code we write and the browser being controlled. In other words, WebDriver allows our code to communicate with the browser so the automation process can run in a real environment.
### Example: Using Selenium WebDriver
The following is a simple example of using **Selenium WebDriver** to open Chrome, visit a webpage, and then close the browser:
```python
from selenium import webdriver
# Create a WebDriver instance for Chrome
driver = webdriver.Chrome()
# Open the selenium.dev webpage
driver.get("http://selenium.dev")
# Close the browser when done
driver.quit()
```
In Django, Selenium is often used in **unit testing** to ensure that the app behaves as expected from the user’s perspective. Unlike regular unit tests, Selenium can **actually open a browser**, click buttons, fill in forms, and check the results—allowing you to test the entire workflow realistically.
### Example in Django
```python
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.by import By
class LoginTest(LiveServerTestCase):
def setUp(self):
# Launch the browser (e.g., Chrome)
self.browser = webdriver.Chrome()
def tearDown(self):
# Close the browser after test finishes
self.browser.quit()
def test_login_page(self):
# Open the login page
self.browser.get(f"{self.live_server_url}/login/")
# Find username & password fields
username_input = self.browser.find_element(By.NAME, "username")
password_input = self.browser.find_element(By.NAME, "password")
# Fill them with sample data
username_input.send_keys("admin")
password_input.send_keys("password123")
# Submit the form
password_input.submit()
# Verify that "Dashboard" appears in the page title after login
self.assertIn("Dashboard", self.browser.title)
```
And voila!! You’ve successfully used **Selenium** in your Django project 🎉.
## Final Words
Congratulations! You’ve successfully completed **Tutorial 3** 😄
After following the entire tutorial above, you should now have a clearer understanding of **forms, authentication, sessions, and cookies** in the Django framework.
1. By the end of this tutorial, your web app should look like this:
- Login page

- Page after successfully logging in (example)

2. Your final local project structure should look like this:

3. Before proceeding, **make sure your local directory structure is correct**. Then, run `add`, `commit`, and `push` to update your GitHub repository.
4. Use the following commands:
```shell
git add .
git commit -m "<commit_message>"
git push -u origin <main_branch>
```
- Replace `<commit_message>` with a descriptive message. Example:
`git commit -m "finished tutorial 3"`
- Replace `<main_branch>` with the name of your main branch (e.g., `main` or `master`). Example: `git push -u origin main` or `git push -u origin master`.
::: danger
When writing commit messages, make sure they are **clear, concise, and formal**. This helps other developers easily understand the purpose and changes in your commit.
:::
## Additional References
- [Session & Cookies](https://www.youtube.com/watch?v=64veb6tKTm0&t=10s)
- [Cookies History](https://www.youtube.com/watch?v=I01XMRo2ESg)
- [Cookies vs. Local Storage vs. Session](https://www.youtube.com/watch?v=AwicscsvGLg)
- [Selenium Documentation](https://www.selenium.dev/documentation/)
## Contributors
- Marla Marlena (NDF)
- Farrell Zidane Raihandrawan (REL)
- Ezar Akhdan (EZR)
- Nevin Thang (FDN)
- Muhammad Milian Alkindi (MMA)
- Grace Karina (GAE)
## Credits
This tutorial was developed based on [PBP Odd Semester 2025](https://github.com/pbp-fasilkom-ui/ganjil-2025) and [PBP Odd Semester 2024](https://github.com/pbp-fasilkom-ui/ganjil-2024), written by the Teaching Team and Teaching Assistants of the **Platform-Based Programming** course in 2025 and 2024. All tutorials and instructions in this repository are specifically designed to help students enrolled in the **Platform-Based Programming** course complete the tutorials during their lab sessions.