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:
parent
59b41c01df
commit
e19781d7ac
15 changed files with 248 additions and 51 deletions
|
@ -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')),
|
||||
|
||||
|
|
|
@ -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')})
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
15
documentation/docs/configuration/social_auth.md
Normal file
15
documentation/docs/configuration/social_auth.md
Normal 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.
|
52
documentation/docs/configuration/social_auth/authentik.md
Normal file
52
documentation/docs/configuration/social_auth/authentik.md
Normal 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
|
||||
|
||||

|
||||
|
||||
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.
|
0
documentation/docs/configuration/social_auth/github.md
Normal file
0
documentation/docs/configuration/social_auth/github.md
Normal file
0
documentation/docs/configuration/social_auth/oidc.md
Normal file
0
documentation/docs/configuration/social_auth/oidc.md
Normal file
BIN
documentation/public/authentik_settings.png
Normal file
BIN
documentation/public/authentik_settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
1
frontend/src/app.d.ts
vendored
1
frontend/src/app.d.ts
vendored
|
@ -15,6 +15,7 @@ declare global {
|
|||
profile_pic: string | null;
|
||||
uuid: string;
|
||||
public_profile: boolean;
|
||||
has_password: boolean;
|
||||
} | null;
|
||||
locale: string;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -9,6 +9,7 @@ export type User = {
|
|||
profile_pic: string | null;
|
||||
uuid: string;
|
||||
public_profile: boolean;
|
||||
has_password: boolean;
|
||||
};
|
||||
|
||||
export type Adventure = {
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue