Rails share database with django

You can share rails user table (generated by devise gem) on django.

Code

User class based on AbstractBaseUser.

from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _


class UserManager(BaseUserManager):
    def create_user(self, email, name, password=None):
        if not email:
            raise ValueError(_('Users must have an email address'))

        user = self.model(email=self.normalize_email(email), name=name)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, nickname, last_name, first_name, password):
        pass  # ref: createsuperuser is hard coded 


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(max_length=70, unique=True)
    name = models.CharField(max_length=70)
    roles = ArrayField(models.CharField(max_length=70), default=[])
    encrypted_password = models.CharField(max_length=128)
    last_login = models.DateTimeField(_('last login'), blank=True, null=True, db_column='last_sign_in_at')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['name', 'password']

    objects = UserManager()

    class Meta:
        managed = False
        db_table = 'users'  

    @property
    def password(self):
        return f'devise{self.encrypted_password}'

    @password.setter
    def password(self, var):
        self.encrypted_password = var[6:]

    @property
    def is_superuser(self):
        print('TODO calculate is_superuser from roles')
        return True

    @property
    def is_active(self):
        print('TODO calculate is_superuser from roles')
        return True   

    @property
    def is_staff(self):
        return True 

Very simple form for admin site. (forms.py)

from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField

from .models import User


class UserCreationForm(forms.ModelForm):
    password = forms.CharField(label='Password', widget=forms.PasswordInput)

    class Meta:
        model = User
        fields = ('email', )

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password"])
        if commit:
            user.save()
        return user


class UserChangeForm(forms.ModelForm):
    password = ReadOnlyPasswordHashField()

    class Meta:
        model = User
        fields = ('email', 'password')

    def clean_password(self):
        return self.initial["password"]

Simple admin class. (admin.py)

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .forms import UserChangeForm, UserCreationForm
from .models import User


class UserAdmin(BaseUserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm
    list_display = ('email', 'name', )
    list_filter = ('roles',)
    search_fields = ('email',)
    ordering = ('email',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
    )

admin.site.register(User, UserAdmin)

Hasher class (modified from BCryptSHA256PasswordHasher).

import hashlib
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import constant_time_compare
        
        
class HERAPasswordHasher(BasePasswordHasher):
    algorithm = "devise"
    digest = hashlib.sha256
    library = ("bcrypt", "bcrypt")
    rounds = 10

    def salt(self):
        bcrypt = self._load_library()
        return bcrypt.gensalt(self.rounds, b'2a')   # devise uses 2a
        
    def encode(self, password, salt):
        bcrypt = self._load_library()
        password = password.encode()
        data = bcrypt.hashpw(password, salt)
        return "%s%s" % (self.algorithm, data.decode('ascii'))

    def decode(self, encoded):
        algorithm, algostr, work_factor, data = encoded.split('$', 4)
        assert algorithm == self.algorithm
        return {
            'algorithm': algorithm,
            'algostr': algostr,
            'checksum': data[22:],
            'salt': data[:22],
            'work_factor': int(work_factor),
        }

    def verify(self, password, encoded):
        algorithm, data = encoded.split('$', 1)
        assert algorithm == self.algorithm
        encoded_2 = self.encode(password, b'$'+data.encode('ascii')[0:28])   # devise uses 29 chars as salt
        return constant_time_compare(encoded, encoded_2)

Now you can modify settings. (I’ve put files above in ruser app.)

AUTH_USER_MODEL = 'ruser.User'
PASSWORD_HASHERS = [
    'ruser.hashers.HERAPasswordHasher',
]

Test

You can test in rails console (bundle exec rails console) and django console (python manager.py shell).

# create user in rails
User.create!({email: "YOUR_EMAIL", password: "YOUR_PASSWORD", 
              password_confirmation: "YOUR_PASSWORD", name: "YOUR_NAME"}) 

# login in rails
ApplicationController.allow_forgery_protection = false
app.post('/users/login', {params: {"user"=>{"email"=>"YOUR_EMAIL", "password"=>"YOUR_PASSWORD"}}})
# login in django
from django.contrib.auth import authenticate
user = authenticate(username='YOUR_EMAIL', password='YOUR_PASSWORD')

# create user in django
from herauser.models import User
User.objects.create_user('YOUR_EMAIL', name='YOUR_NAME', password='YOUR_PASSWORD')

# update password in django
from herauser.models import User
u = User.objects.filter(email='YOUR_EMAIL')[0]
u.set_password('YOUR_PASSWORD')
u.save()

korean original post is https://tech.jinto.pe.kr/post/2021-05-23-%EB%A0%88%EC%9D%BC%EC%A6%88-%EC%82%AC%EC%9A%A9%EC%9E%90%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%9E%A5%EA%B3%A0/