1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 12:59:36 +02:00

feat: add public URL endpoint and update user type to include password status

This commit is contained in:
Sean Morley 2025-01-06 18:53:08 -05:00
parent 59b41c01df
commit e19781d7ac
15 changed files with 248 additions and 51 deletions

View file

@ -4,7 +4,7 @@ from django.views.generic import RedirectView, TemplateView
from django.conf import settings
from django.conf.urls.static import static
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView
from .views import get_csrf_token
from .views import get_csrf_token, get_public_url
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
@ -30,6 +30,7 @@ urlpatterns = [
path('auth/social-providers/', EnabledSocialProvidersView.as_view(), name='enabled-social-providers'),
path('csrf/', get_csrf_token, name='get_csrf_token'),
path('public-url/', get_public_url, name='get_public_url'),
path('', TemplateView.as_view(template_name='home.html')),

View file

@ -1,6 +1,10 @@
from django.http import JsonResponse
from django.middleware.csrf import get_token
from os import getenv
def get_csrf_token(request):
csrf_token = get_token(request)
return JsonResponse({'csrfToken': csrf_token})
def get_public_url(request):
return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')})

View file

@ -36,7 +36,7 @@ import os
class UserDetailsSerializer(serializers.ModelSerializer):
"""
User model w/o password
User model without exposing the password.
"""
@staticmethod
@ -49,8 +49,8 @@ class UserDetailsSerializer(serializers.ModelSerializer):
return username
class Meta:
model = CustomUser
extra_fields = ['profile_pic', 'uuid', 'public_profile']
profile_pic = serializers.ImageField(required=False)
if hasattr(UserModel, 'USERNAME_FIELD'):
extra_fields.append(UserModel.USERNAME_FIELD)
@ -64,19 +64,14 @@ class UserDetailsSerializer(serializers.ModelSerializer):
extra_fields.append('date_joined')
if hasattr(UserModel, 'is_staff'):
extra_fields.append('is_staff')
if hasattr(UserModel, 'public_profile'):
extra_fields.append('public_profile')
class Meta:
model = CustomUser
fields = ('profile_pic', 'uuid', 'public_profile', 'email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
model = UserModel
fields = ('pk', *extra_fields)
fields = ['pk', *extra_fields]
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
def handle_public_profile_change(self, instance, validated_data):
"""Remove user from `shared_with` if public profile is set to False."""
"""
Remove user from `shared_with` if public profile is set to False.
"""
if 'public_profile' in validated_data and not validated_data['public_profile']:
for collection in Collection.objects.filter(shared_with=instance):
collection.shared_with.remove(instance)
@ -91,20 +86,37 @@ class UserDetailsSerializer(serializers.ModelSerializer):
class CustomUserDetailsSerializer(UserDetailsSerializer):
"""
Custom serializer to add additional fields and logic for the user details.
"""
has_password = serializers.SerializerMethodField()
class Meta(UserDetailsSerializer.Meta):
model = CustomUser
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid',)
fields = UserDetailsSerializer.Meta.fields + ['profile_pic', 'uuid', 'public_profile', 'has_password']
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password')
@staticmethod
def get_has_password(instance):
"""
Computes whether the user has a usable password set.
"""
return instance.has_usable_password()
def to_representation(self, instance):
"""
Customizes the serialized output to modify `profile_pic` URL and add computed fields.
"""
representation = super().to_representation(instance)
# Construct profile picture URL if it exists
if instance.profile_pic:
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
#print(public_url)
# remove any ' from the url
public_url = public_url.replace("'", "")
public_url = public_url.replace("'", "") # Sanitize URL
representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}"
del representation['pk'] # remove the pk field from the response
# Remove `pk` field from the response
representation.pop('pk', None)
return representation

View file

@ -91,6 +91,28 @@ export default defineConfig({
text: "Immich Integration",
link: "/docs/configuration/immich_integration",
},
{
text: "Social Auth",
link: "/docs/configuration/social_auth",
},
{
text: "Authentication Providers",
collapsed: false,
items: [
{
text: "Authentik",
link: "/docs/configuration/social_auth/authentik",
},
{
text: "GitHub",
link: "/docs/configuration/social_auth/github",
},
{
text: "Open ID Connect",
link: "/docs/configuration/social_auth/oidc",
},
],
},
{
text: "Update App",
link: "/docs/configuration/updating",

View file

@ -0,0 +1,15 @@
# Social Authentication
AdventureLog support autentication via 3rd party services and self-hosted identity providers. Once these services are enabled, users can log in to AdventureLog using their accounts from these services and link exising AdventureLog accounts to these services for easier access.
The steps for each service varies so please refer to the specific service's documentation for more information.
## Supported Services
- [Authentik](social_auth/authentik.md) (self-hosted)
- [GitHub](social_auth/github.md)
- [Open ID Connect](social_auth/oidc.md)
## Linking Existing Accounts
If you already have an AdventureLog account and would like to link it to a 3rd party service, you can do so by logging in to AdventureLog and navigating to the `Account Settings` page. From there, scroll down to `Social and OIDC Authentication` and click the `Launch Account Connections` button. If identity providers have been enabled on your instance, you will see a list of available services to link to.

View file

@ -0,0 +1,52 @@
# Authentik Social Authentication
<img src="https://repository-images.githubusercontent.com/230885748/19f01d00-8e26-11eb-9a14-cf0d28a1b68d" alt="Authentik Logo" width="400" />
Authentik is a self-hosted identity provider that supports OpenID Connect and OAuth2. AdventureLog can be configured to use Authentik as an identity provider for social authentication. Learn more about Authentik at [goauthentik.io](https://goauthentik.io/).
Once Authentik is configured by the administrator, users can log in to AdventureLog using their Authentik account and link existing AdventureLog accounts to Authentik for easier access.
# Configuration
To enable Authentik as an identity provider, the administrator must first configure Authentik to allow AdventureLog to authenticate users.
### Authentik Configuration
1. Log in to Authentik and navigate to the `Providers` page and create a new provider.
2. Select `OAuth2/OpenID Provider` as the provider type.
3. Name it `AdventureLog` or any other name you prefer.
4. Set the `Redirect URI` of type `Regex` to `^http://<adventurelog-server-url>/accounts/oidc/.*$` where `<adventurelog-url>` is the URL of your AdventureLog Server service.
5. Copy the `Client ID` and `Client Secret` generated by Authentik, you will need these to configure AdventureLog.
6. Create an application in Authentik and assign the provider to it, name the `slug` `adventurelog` or any other name you prefer.
### AdventureLog Configuration
This configuration is done in the [Admin Panel](../../guides/admin_panel.md). You can either launch the pannel directly from the `Settings` page or navigate to `/admin` on your AdventureLog server.
1. Login to AdventureLog as an administrator and navigate to the `Settings` page.
2. Scroll down to the `Administration Settings` and launch the admin panel.
3. In the admin panel, navigate to the `Social Accounts` section and click the add button next to `Social applications`. Fill in the following fields:
- Provider: `OpenID Connect`
- Provider ID: Autnentik Client ID
- Name: `Authentik`
- Client ID: Authentik Client ID
- Secret Key: Authentik Client Secret
- Key: can be left blank
- Settings: (make sure http/https is set correctly)
```json
{
"server_url": "http://<authentik_url>/application/o/[YOUR_SLUG]/"
}
```
- Sites: move over the sites you want to enable Authentik on, usually `example.com` and `www.example.com` unless you renamed your sites.
#### What it Should Look Like
![Authentik Social Auth Configuration](/authentik_settings.png)
4. Save the configuration and restart the AdventureLog server.
Ensure that the Authentik server is running and accessible by AdventureLog. Users should now be able to log in to AdventureLog using their Authentik account.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

@ -15,6 +15,7 @@ declare global {
profile_pic: string | null;
uuid: string;
public_profile: boolean;
has_password: boolean;
} | null;
locale: string;
}

View file

@ -30,8 +30,9 @@
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y-missing-attribute -->
<p class="text-lg ml-4 font-bold">
{$t('navbar.greeting')}, {user.first_name}
{user.last_name}
{$t('navbar.greeting')}, {user.first_name
? `${user.first_name} ${user.last_name}`
: user.username}
</p>
<li><button on:click={() => goto('/profile')}>{$t('navbar.profile')}</button></li>
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>

View file

@ -9,6 +9,7 @@ export type User = {
profile_pic: string | null;
uuid: string;
public_profile: boolean;
has_password: boolean;
};
export type Adventure = {

View file

@ -17,7 +17,11 @@
<div class="container mx-auto p-4">
<!-- Welcome Message -->
<div class="mb-8">
<h1 class="text-4xl font-extrabold">{$t('dashboard.welcome_back')}, {user?.first_name}!</h1>
<h1 class="text-4xl font-extrabold">
{$t('dashboard.welcome_back')}, {user?.first_name
? `${user.first_name} ${user.last_name}`
: user?.username}!
</h1>
</div>
<!-- Stats -->

View file

@ -66,12 +66,22 @@ export const load: PageServerLoad = async (event) => {
immichIntegration = await immichIntegrationsFetch.json();
}
let publicUrlFetch = await fetch(`${endpoint}/public-url/`);
let publicUrl = '';
if (!publicUrlFetch.ok) {
return redirect(302, '/');
} else {
let publicUrlJson = await publicUrlFetch.json();
publicUrl = publicUrlJson.PUBLIC_URL;
}
return {
props: {
user,
emails,
authenticators,
immichIntegration
immichIntegration,
publicUrl
}
};
};
@ -179,33 +189,52 @@ export const actions: Actions = {
const password1 = formData.get('password1') as string | null | undefined;
const password2 = formData.get('password2') as string | null | undefined;
const current_password = formData.get('current_password') as string | null | undefined;
let current_password = formData.get('current_password') as string | null | undefined;
if (password1 !== password2) {
return fail(400, { message: 'settings.password_does_not_match' });
}
if (!current_password) {
return fail(400, { message: 'settings.password_is_required' });
current_password = null;
}
let csrfToken = await fetchCSRFToken();
let res = await fetch(`${endpoint}/_allauth/browser/v1/account/password/change`, {
method: 'POST',
headers: {
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
current_password,
new_password: password1
})
});
if (!res.ok) {
return fail(res.status, { message: 'settings.error_change_password' });
if (current_password) {
let res = await fetch(`${endpoint}/_allauth/browser/v1/account/password/change`, {
method: 'POST',
headers: {
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
current_password,
new_password: password1
})
});
if (!res.ok) {
return fail(res.status, { message: 'settings.error_change_password' });
}
return { success: true };
} else {
let res = await fetch(`${endpoint}/_allauth/browser/v1/account/password/change`, {
method: 'POST',
headers: {
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
new_password: password1
})
});
if (!res.ok) {
console.log('Error:', await res.json());
return fail(res.status, { message: 'settings.error_change_password' });
}
return { success: true };
}
return { success: true };
},
changeEmail: async (event) => {
if (!event.locals.user) {

View file

@ -9,6 +9,7 @@
import TotpModal from '$lib/components/TOTPModal.svelte';
import { appTitle, appVersion } from '$lib/config.js';
import ImmichLogo from '$lib/assets/immich.svg';
import { goto } from '$app/navigation';
export let data;
console.log(data);
@ -20,7 +21,7 @@
}
let new_email: string = '';
let public_url: string = data.props.publicUrl;
let immichIntegration = data.props.immichIntegration;
let newImmichIntegration: ImmichIntegration = {
@ -307,17 +308,19 @@
</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md">
<form method="post" action="?/changePassword" use:enhance class="space-y-6">
<div>
<label for="current_password" class="text-sm font-medium text-neutral-content"
>{$t('settings.current_password')}</label
>
<input
type="password"
id="current_password"
name="current_password"
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
{#if user.has_password}
<div>
<label for="current_password" class="text-sm font-medium text-neutral-content"
>{$t('settings.current_password')}</label
>
<input
type="password"
id="current_password"
name="current_password"
class="block w-full mt-1 input input-bordered input-primary"
/>
</div>
{/if}
<div>
<label for="password1" class="text-sm font-medium text-neutral-content"
@ -424,6 +427,58 @@
</div>
</section>
<!-- Admin Settings -->
{#if user.is_staff}
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">Administration Settings</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
<a class="btn btn-primary mt-4" href={`${public_url}/admin/`} target="_blank"
>Launch Administration Pannel</a
>
</div>
</section>
{/if}
<!-- Social Auth Settings -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">Social and ODIC Authentication</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
<p>
Enable or disable social and OIDC authentication providers for your account. These
connections allow you to sign in with self hosted authentication identity providers like
Authentik or 3rd party providers like GitHub.
</p>
<div role="alert" class="alert alert-info mt-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span
>These settings are managed in the AdventureLog server and must be manually enabled by the
administrator. <a
href="https://adventurelog.app/docs/configuration/social_auth.html"
class="link link-neutral"
target="_blank">Documentation Link</a
>.
</span>
</div>
<a
class="btn btn-primary mt-4"
href={`${public_url}/accounts/social/connections/`}
target="_blank">Launch Account Connections</a
>
</div>
</section>
<!-- Immich Integration Section -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">