For most applications, user is a must, we always need to develop user login, signup, query and update APIs. This post will walk you through a concise and flexible 100+ lines of Python code to implement such APIs.
We will use UtilMeta framework to build these APIs, It's an open-source Python meta backend framework, which supports the integration of Python frameworks like Django, Flask, FastAPI, and efficiently builds declarative RESTful APIs based on the Python type annotation standard
0. Installation
You can install UtilMeta framework using the following command
pip install utilmeta
UtilMeta requires Python >= 3.8
1. Create project
We use the meta setup
command to create a new project.
meta setup demo-user
We will use Django as runtime framework of this project, so you can enter django
when prompted to select backend
After the project is created, we need to configure the database connection of the service, open server.py
, and insert the following code after the declaration of service
service = UtilMeta(
__name__,
name='demo-user',
backend=django,
)
# new +++++
from utilmeta.core.server.backends.django import DjangoSettings
from utilmeta.core.orm import DatabaseConnections, Database
service.use(DjangoSettings(
secret_key='YOUR_SECRET_KEY',
))
service.use(DatabaseConnections({
'default': Database(
name='db',
engine='sqlite3',
)
}))
In the inserted code, we declare the configuration information of Django and the configuration of the database connection. Because Django uses the app to manage the data model, we use the following command to create an app named user
meta add user
You can see that a new folder named user
has been created in our project folder, which includes
/user
/migrations
api.py
models.py
schema.py
The migrations
folder is where Django handles the database migrations of the models
Once the app is created, we insert a line into the DjangoSettings
of server.py
to specify the app.
service.use(DjangoSettings(
secret_key='YOUR_SECRET_KEY',
apps=['user']
))
So far, we have completed the configuration and initialization of the project.
2. Write user model
The user APIs depend on the "user", so before developing the API, we should write the user’s data model. We open user/models.py
and write
from django.db import models
from utilmeta.core.orm.backends.django.models import AbstractSession, PasswordField
class User(models.Model):
username = models.CharField(max_length=20, unique=True)
password = PasswordField(max_length=100)
signup_time = models.DateTimeField(auto_now_add=True)
class Session(AbstractSession):
user = models.ForeignKey(
User, related_name='sessions',
null=True, default=None,
on_delete=models.CASCADE
)
We write a User
model firstly that contains
-
username
: The username field which is required to be unique (unique=True
) -
password
: The password field usingPasswordField
to auto-encrypt the input password (with pbkdf2) -
signup_time
: The signup time field
In addition to the User
model, we have also written a Session
model for users to record user sessions and login state. We will implement user login and authentication through this model.
Connect database
After we write the data model, we can use the migration command provided by Django to easily create the corresponding data table. Since we use SQLite, we do not need to install the database software in advance. We only need to run the following two commands to complete the creation of the database.
meta makemigrations
meta migrate
When you see the following output, you have finished creating the database
Running migrations:
Applying contenttypes.0001_initial... OK
Applying user.0001_initial... OK
The database migration command created a SQLite database named db
in the project folder according to the database configuration in server.py
, where the table of User and Session models has been created
3. Session and Authentication
After writing the user models, we can start to develop the authentication logic. We create a new file named auth.py
in the user folder and write the configuration of session and user authentication.
from utilmeta.core import auth
from utilmeta.core.auth.session.db import DBSessionSchema, DBSession
from .models import Session, User
USER_ID = '_user_id'
class SessionSchema(DBSessionSchema):
def get_session_data(self):
data = super().get_session_data()
data.update(user_id=self.get(USER_ID))
return data
session_config = DBSession(
session_model=Session,
engine=SessionSchema,
cookie=DBSession.Cookie(
name='sessionid',
age=7 * 24 * 3600,
http_only=True
)
)
user_config = auth.User(
user_model=User,
authentication=session_config,
key=USER_ID,
login_fields=User.username,
password_field=User.password,
)
In this code, SessionSchema
is the core engine that processes and stores Session data, session_config
declares the Session configuration with Session
model and engine we just wrote, and configures the corresponding Cookie policy
We use session store based on database to simply our tutorial, in practice, we often use cache+db as the store, you can find more in Session Authentication
We also declare the user authentication configuration user_config
with the following params
-
user_model
: Specify the user model for authentication, which is the User model I wrote in the previous section. -
authentication
: Specify the authentication method. We passsession_config
in to declare that user authentication is performed using Session. -
key
: Specify the key of the current user ID in the session data -
login_fields
: Fields that can be used for login, such as username, email, etc., which need to be unique. -
password_field
: The user’s password field. Declaring these allows UtilMeta to automatically handle the login verification logic for you.
4. Write user API
Signup API
First, we will write the user signup API. It should receive the user name and password fields, complete the signup after verifying that the user name is not occupied, and return the newly registered user data.
We open the user/api.py
and write
from datetime import datetime
from utilmeta.core import api, orm
from utilmeta.utils import exceptions
from .models import User
from . import auth
class SignupSchema(orm.Schema[User]):
username: str
password: str
class UserSchema(orm.Schema[User]):
id: int
username: str
signup_time: datetime
@auth.session_config.plugin
class UserAPI(api.API):
@api.post
def signup(self, data: SignupSchema = request.Body) -> UserSchema:
if User.objects.filter(username=data.username).exists():
raise exceptions.BadRequest('Username exists')
data.save()
auth.user_config.login_user(
request=self.request,
user=data.get_instance()
)
return UserSchema.init(data.pk)
We use @api
decorator to define the API functions that provide API service, which contains HTTP methods like GET / POST / PUT / PATCH / DELETE, we are using the POST method in the signup API. You can use the first param in the decorator to specify the API's path, if empty like the above example, the API's path will be the function's name (signup
)
We declared the request body of signup API as SignupSchema
. so that UtilMeta will automatically parse and covert request body to a SignupSchema
instance, the invalid request will be rejected by UtilMeta and with a 400 response
The logic in the signup API function is
- Detect whether the in
username
the request has been registered - Call
data.save()
method to save the signup data - Login the registered user to the current request using
login_user
- Returns after initializing the new user’s data to a UserSchema instance using
UserSchema.init(data.pk)
UtilMeta has developed an efficient declarative ORM mechanism, We use
orm.Schema[User]
to define a Schema class with theUser
model injected, so that we can use the methods of the schema class to create, update, and serialize data. You can find more in Data Query and ORM Document
We can also find that a decorator named @auth.session_config.plugin
is plugin to the UserAPI
class. This is where the Session configuration is applied to the API. This plugin can save the Session data after each request and patch the response with corresponding Set-Cookie
header
Login & Logout API
Next, we'll write the user’s login and logout APIs
from datetime import datetime
from utilmeta.core import api, orm, request
from utilmeta.utils import exceptions
from .models import User
from . import auth
import utype
class LoginSchema(utype.Schema):
username: str
password: str
@auth.session_config.plugin
class UserAPI(api.API):
@api.post
def signup(self): ...
# new ++++
@api.post
def login(self, data: LoginSchema = request.Body) -> UserSchema:
user = auth.user_config.login(
request=self.request,
ident=data.username,
password=data.password
)
if not user:
raise exceptions.PermissionDenied('Username of password wrong')
return UserSchema.init(user)
@api.post
def logout(self, session: auth.SessionSchema = auth.session_config):
session.flush()
In the login API, we call the login()
method in our authentication configuration to complete the login simply. Since we have configured the login field and password field, UtilMeta can help us complete the password verification and login automatically. If the login is successful, the corresponding user instance is returned. So we can throw an error if the login()
result is None, and after a successful login, we can call UserSchema.init
to return the login user data to the client.
For the logout API, we just need to empty the session data of current request, We use the declared session_config
as the default of the function parameter to receive the Session object of the current request and use session.flush()
to empty it.
Get & Update user data
When we understand the usage of declarative ORM, it is very simple to write the get & update API of User
from datetime import datetime
from utilmeta.core import api, orm, request
from utilmeta.utils import exceptions
from .models import User
from . import auth
import utype
class UserUpdateSchema(orm.Schema[User]):
id: int = orm.Field(no_input=True)
username: str = orm.Field(required=False)
password: str = orm.Field(required=False)
@auth.session_config.plugin
class UserAPI(api.API):
@api.post
def signup(self): ...
@api.post
def login(self): ...
@api.post
def logout(self): ...
# new ++++
def get(self, user: User = auth.user_config) -> UserSchema:
return UserSchema.init(user)
def put(self, data: UserUpdateSchema = request.Body,
user: User = auth.user_config) -> UserSchema:
data.id = user.pk
data.save()
return UserSchema.init(data.pk)
After we declare the user authentication configuration, we can declare user: User = auth.user_config
in the API function parameters to get the instance of the current request user in any API that requires user login. If the request is not logged in, UtilMeta will automatically process and return a 401 Unauthorized
response
In the get
API, we directly serialize the current request user using UserSchema
and return it to the client
In the put
API, we assign the current request user's ID to the id
field of UserUpdateSchema
, and return the updated user data after saving.
Since we can’t allow the requesting user to arbitrarily specify the user ID to be updated, we use the no_input=True
option for id
field, which is actually a common practice, that is a user can only update his own information.
If your API function using the name of HTTP methods (such as get/put/patch/post/delete), it will bind the method and mount the same route of the API class, these methods are called core methods of the API class
At this point, our API is all developed.
Mount API
To provide access to our developed UserAPI, we need to mount it on the root API of the service. Let’s go back to server.py
and modify the declaration of the RootAPI.
# new +++
service.setup()
from user.api import UserAPI
class RootAPI(api.API):
user: UserAPI
service.mount(RootAPI, route='/api')
We mount the developed UserAPI to the RootAPI's user
property, which means that the UserAPI
's path is mounted to /api/user
, the endpoints in UserAPI
will follow the path, like
-
GET /api/user
: Get the current user of the request -
PUT /api/user
: Update the current user of the request -
POST /api/user/login
: User login -
POST /api/user/logout
: User logout -
POST /api/user/signup
: User signup
This mounting syntax is convenient for defining tree-like API structure
You should call
service.setup()
before import any Django models to complete the setup of Django
5. Run the API
Run the API service using the following command in the project folder
meta run
Or you can use
python server.py
When you see the following output, the service has started successfully
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
You can alter the
host
andport
params of UtilMeta service inserver.py
to change the address of the API service
6. Debug API
After starting our API service, we can debug the APIs using the client in UtilMeta, let's create a new file named test.py
in the project directory and write
from server import service
if __name__ == '__main__':
with service.get_client(live=True) as client:
r1 = client.post('user/signup', data={
'username': 'user1',
'password': '123123'
})
r1.print()
r2 = client.get('user')
r2.print()
It contains the debug code for the signup and gets APIs, when we started the service and run test.py
, we can see the following output like
Response [200 OK] "POST /api/user/signup"
application/json (76)
{'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T12:29:33.684594'}
Response [200 OK] "GET /api/user"
application/json (76)
{'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T12:29:33.684594'}
It means our signup and get APIs worked,
In the
with
code block, client will store theSet-Cookie
in the response the send in the following requests, so we can see the session works just like the browser
We can also test the login, logout, and update APIs, the complete examples are as follows
from server import service
if __name__ == '__main__':
with service.get_client(live=True) as client:
r1 = client.post('user/signup', data={
'username': 'user1',
'password': '123123'
})
r1.print()
# Response [200 OK] "POST /api/user/signup"
# application/json (75)
# {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
r2 = client.get('user')
r2.print()
# Response [200 OK] "GET /api/user"
# application/json (75)
# {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
r3 = client.post('user/logout')
r3.print()
# Response [200 OK] "POST /api/user/logout"
# text/html (0)
r4 = client.get('user')
r4.print()
# Response [401 Unauthorized] "GET /api/user"
# text/html (0)
r5 = client.post('user/login', data={
'username': 'user1',
'password': '123123'
})
# Response [200 OK] "POST /api/user/login"
# application/json (75)
# {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
r5.print()
r6 = client.get('user')
r6.print()
# Response [200 OK] "GET /api/user"
# application/json (75)
# {'username': 'user1', 'id': 1, 'signup_time': '2024-01-29T13:29:03.336134'}
r7 = client.put('user', data={
'username': 'user-updated',
'password': '123456'
})
r7.print()
# Response [200 OK] "PUT /api/user"
# application/json (82)
# {'username': 'user-updated', 'id': 1, 'signup_time': '2024-01-29T13:44:30.095711'}
References
- Source Codeof this post
- UtilMeta: framework used in this post
If you have any problem, you are welcome to join the discord server of UtilMeta to discuss
Top comments (2)
Signup, login and logout, with a username and password, that's "a good start" but it's far from being enough:
Beyond the login page
Thomas Broyer ・ Nov 29 '23
Agreed, this post is just a beginner tutorial that only covered the simplest usage, I'll definitely write a "good practice" of real-world login & authentication post later