mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Feature: OIDC & social authentication (#5190)
--------- Co-authored-by: Moritz Pflanzer <moritz@chickadee-engineering.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		| @@ -18,7 +18,8 @@ | ||||
|   </head> | ||||
| 
 | ||||
|   <body class="text-center"> | ||||
|     <form class="form-signin position-absolute top-50 start-50 translate-middle" method="post"> | ||||
|     <div class="position-absolute top-50 start-50 translate-middle"> | ||||
|     <form class="form-signin" method="post"> | ||||
| 			{% csrf_token %} | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" width="300" class="logo mb-4" viewBox="0 0 2897.4 896.6"> | ||||
|         <path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/> | ||||
| @@ -38,6 +39,11 @@ | ||||
|           <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/> | ||||
|         </g> | ||||
|       </svg> | ||||
|       {% for message in messages %} | ||||
|         <div class="alert alert-{{ message.level_tag }}" role="alert"> | ||||
|           {{ message }} | ||||
|         </div> | ||||
|       {% endfor %} | ||||
| 			<p>{% translate "Please sign in." %}</p> | ||||
| 			{% if form.errors %} | ||||
|         <div class="alert alert-danger" role="alert"> | ||||
| @@ -55,7 +61,7 @@ | ||||
| 			{% translate "Username" as i18n_username %} | ||||
| 			{% translate "Password" as i18n_password %} | ||||
|       <div class="form-floating"> | ||||
|         <input type="text" name="username" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus> | ||||
|         <input type="text" name="login" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus> | ||||
|         <label for="inputUsername">{{ i18n_username }}</label> | ||||
|       </div> | ||||
|       <div class="form-floating"> | ||||
| @@ -67,9 +73,33 @@ | ||||
|       </div> | ||||
|       {% if EMAIL_ENABLED %} | ||||
|       <div class="d-grid mt-3"> | ||||
|         <a class="btn btn-link" href="{% url 'password_reset' %}">{% translate "Forgot your password?" %}</a> | ||||
|         <a class="btn btn-link" href="{% url 'account_reset_password' %}">{% translate "Forgot your password?" %}</a> | ||||
|       </div> | ||||
|       {% endif %} | ||||
| 		</form> | ||||
| {% load allauth socialaccount %} | ||||
| {% get_providers as socialaccount_providers %} | ||||
| {% if socialaccount_providers %} | ||||
|     <p class="mt-3">{% translate "or sign in via" %}</p> | ||||
|     <ul class="m-0 p-0"> | ||||
|         {% for provider in socialaccount_providers %} | ||||
|             {% if provider.id == "openid" %} | ||||
|                 {% for brand in provider.get_brands %} | ||||
|                     {% provider_login_url provider openid=brand.openid_url process=process as href %} | ||||
|                 <li class="d-grid mt-3"><a class="btn btn-secondary" href="{{ href }}">{{ brand.name }}</a></li> | ||||
|                 {% endfor %} | ||||
|             {% else %} | ||||
|             {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} | ||||
|             <li class="d-grid mt-3"> | ||||
|               <form class="d-grid" method="POST" action="{{ href }}"> | ||||
|                 {% csrf_token %} | ||||
|                 <button type="submit" class="btn btn-secondary">{{ provider.name }}</button> | ||||
|               </form> | ||||
|             </li> | ||||
|             {% endif %} | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
|     </div> | ||||
| 	</body> | ||||
| </html> | ||||
| @@ -2,6 +2,7 @@ | ||||
| 
 | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
| {% load allauth %} | ||||
| 
 | ||||
| <html lang="en"> | ||||
|   <head> | ||||
| @@ -18,7 +19,8 @@ | ||||
|   </head> | ||||
| 
 | ||||
|   <body class="text-center"> | ||||
|     <form class="form-signin position-absolute top-50 start-50 translate-middle" method="post"> | ||||
|     {% url 'account_reset_password' as reset_url %} | ||||
|     <form class="form-signin position-absolute top-50 start-50 translate-middle" method="post" action="{{reset_url}}"> | ||||
| 			{% csrf_token %} | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" width="300" class="logo mb-4" viewBox="0 0 2897.4 896.6"> | ||||
|         <path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/> | ||||
| @@ -47,7 +49,7 @@ | ||||
| 			{% translate "Email" as i18n_email %} | ||||
|       <h1></h1> | ||||
|       <div class="form-floating"> | ||||
|         <input type="email" name="email" id="inputEmail" placeholder="{{ i18n_email }}" class="form-control" required> | ||||
|         <input type="{{form.email.type}}" name="{{form.email.name}}" id="inputEmail" placeholder="{{ i18n_email }}" class="form-control" required> | ||||
| 			  <label for="inputEmail">{{ i18n_email }}</label> | ||||
|       </div> | ||||
|       <div class="d-grid mt-3"> | ||||
| @@ -2,6 +2,7 @@ | ||||
| 
 | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
| {% load allauth %} | ||||
| 
 | ||||
| <html lang="en"> | ||||
|   <head> | ||||
| @@ -38,7 +39,10 @@ | ||||
|           <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/> | ||||
|         </g> | ||||
|       </svg> | ||||
|       {% if validlink %} | ||||
|       {% if token_fail %} | ||||
|         {% url 'account_reset_password' as passwd_reset_url %} | ||||
|         <p>The password reset link was invalid, possibly because it has already been used. Please <a class="btn btn-link" href="{{passwd_reset_url}}">{% translate "request a new password reset" %}</a>.</p> | ||||
|       {% else %} | ||||
| 			<p>{% translate "Set a new password." %}</p> | ||||
| 			{% if form.errors %} | ||||
|         <div class="alert alert-danger" role="alert"> | ||||
| @@ -50,20 +54,16 @@ | ||||
| 			{% translate "Confirm Password" as i18n_new_password2 %} | ||||
|       <h1></h1> | ||||
|       <div class="form-floating"> | ||||
|         <input type="password" name="new_password1" id="inputPassword1" placeholder="{{ i18n_new_password1 }}" class="form-control" required> | ||||
|         <input type="password" name="{{form.password1.name}}" id="inputPassword1" placeholder="{{ i18n_new_password1 }}" class="form-control" required> | ||||
| 			  <label for="inputPassword1">{{ i18n_new_password1 }}</label> | ||||
|       </div> | ||||
|       <div class="form-floating"> | ||||
|         <input type="password" name="new_password2" id="inputPassword2" placeholder="{{ i18n_new_password2 }}" class="form-control" required> | ||||
|         <input type="password" name="{{form.password2.name}}" id="inputPassword2" placeholder="{{ i18n_new_password2 }}" class="form-control" required> | ||||
| 			  <label for="inputPassword2">{{ i18n_new_password2 }}</label> | ||||
|       </div> | ||||
|       <div class="d-grid mt-3"> | ||||
|         <button class="btn btn-lg btn-primary" type="submit">{% translate "Change my password" %}</button> | ||||
|       </div> | ||||
|       {% else %} | ||||
| 
 | ||||
|       <p>The password reset link was invalid, possibly because it has already been used. Please <a class="btn btn-link" href="{% url 'password_reset' %}">{% translate "request a new password reset" %}</a>.</p> | ||||
| 
 | ||||
|       {% endif %} | ||||
| 		</form> | ||||
| 	</body> | ||||
| @@ -38,7 +38,7 @@ | ||||
|           </g> | ||||
|         </svg> | ||||
|         <h3>{% translate "Password reset complete." %}</h3> | ||||
|         {% url 'login' as login_url %} | ||||
|         {% url 'account_login' as login_url %} | ||||
|         <p>{% blocktranslate %}Your new password has been set. You can now <a href="{{ login_url }}">log in</a>{% endblocktranslate %}.</p> | ||||
|       </div> | ||||
| 	</body> | ||||
| @@ -80,6 +80,13 @@ | ||||
| 				<p class="warning m-auto mt-3 small fade hide">{% translate "Still here?! Hmm, something might be wrong." %} <a href="https://docs.paperless-ngx.com">{% translate "Here's a link to the docs." %}</a></p> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<script type="text/javascript">{# Pass Django messages to Angular frontend #} | ||||
| 			window.DJANGO_MESSAGES = [ | ||||
| 				{% for message in messages %} | ||||
| 					{ level: "{{ message.level_tag | escapejs }}", message: "{{ message | escapejs }}" }, | ||||
| 				{% endfor %} | ||||
| 			] | ||||
| 		</script> | ||||
| 	</pngx-root> | ||||
| 	<script src="{% static runtime_js %}" defer></script> | ||||
| 	<script src="{% static polyfills_js %}" defer></script> | ||||
|   | ||||
| @@ -2,23 +2,25 @@ | ||||
| 
 | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
| {% load allauth %} | ||||
| 
 | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|     <meta name="description" content="Paperless-ngx Signed Out"> | ||||
|     <meta name="description" content="Paperless-ngx Sign In"> | ||||
|     <meta name="author" content="Paperless-ngx project and contributors"> | ||||
|     <meta name="robots" content="noindex,nofollow"> | ||||
| 
 | ||||
|     <title>{% translate "Paperless-ngx signed out" %}</title> | ||||
|     <title>{% translate "Paperless-ngx social account sign in" %}</title> | ||||
| 
 | ||||
|     <link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> | ||||
| 		<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> | ||||
|     <link href="{% static 'signin.css' %}" rel="stylesheet"> | ||||
|   </head> | ||||
| 
 | ||||
|   <body class="text-center"> | ||||
|     <div class="position-absolute top-50 start-50 translate-middle"> | ||||
| 			{% csrf_token %} | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" width="300" class="logo mb-4" viewBox="0 0 2897.4 896.6"> | ||||
|         <path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/> | ||||
|         <g class="text" style="fill:#000"> | ||||
| @@ -37,8 +39,8 @@ | ||||
|           <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/> | ||||
|         </g> | ||||
|       </svg> | ||||
| 			<p>{% translate "You have been successfully logged out. Bye!" %}</p> | ||||
| 			<a href="{% url 'base' %}">{% translate "Sign in again" %}</a> | ||||
| 		</div> | ||||
|         {% url 'account_login' as login_url %} | ||||
|       <p>{% blocktranslate %}An error occurred while attempting to login via your social network account. Back to the <a href="{{ login_url }}">login page</a>{% endblocktranslate %}</p> | ||||
|     </div> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										52
									
								
								src/documents/templates/socialaccount/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/documents/templates/socialaccount/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <!doctype html> | ||||
|  | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
| {% load allauth %} | ||||
|  | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|     <meta name="description" content="Paperless-ngx Sign In"> | ||||
|     <meta name="author" content="Paperless-ngx project and contributors"> | ||||
|     <meta name="robots" content="noindex,nofollow"> | ||||
|  | ||||
|     <title>{% translate "Paperless-ngx social account sign in" %}</title> | ||||
|  | ||||
| 		<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> | ||||
|     <link href="{% static 'signin.css' %}" rel="stylesheet"> | ||||
|   </head> | ||||
|  | ||||
|   <body class="text-center"> | ||||
|     <div class="position-absolute top-50 start-50 translate-middle"> | ||||
|     <form class="form-signin" method="post"> | ||||
| 			{% csrf_token %} | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" width="300" class="logo mb-4" viewBox="0 0 2897.4 896.6"> | ||||
|         <path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/> | ||||
|         <g class="text" style="fill:#000"> | ||||
|           <path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/> | ||||
|           <path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/> | ||||
|           <path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/> | ||||
|           <path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z"  transform="translate(0)"/> | ||||
|           <rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/> | ||||
|           <path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/> | ||||
|           <path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/> | ||||
|           <path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/> | ||||
|           <rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/> | ||||
|           <path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/> | ||||
|           <path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/> | ||||
|           <polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5  2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 	" transform="translate(0)"/> | ||||
|           <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/> | ||||
|         </g> | ||||
|       </svg> | ||||
|         <p> | ||||
|             {% blocktrans with provider.name as provider %}You are about to connect a new third-party account from {{ provider }}.{% endblocktrans %} | ||||
|         </p> | ||||
|       <div class="d-grid mt-3"> | ||||
|         <button class="btn btn-lg btn-primary" type="submit">{% translate "Continue" %}</button> | ||||
|       </div> | ||||
| 		</form> | ||||
|     </div> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										77
									
								
								src/documents/templates/socialaccount/signup.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/documents/templates/socialaccount/signup.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| <!doctype html> | ||||
|  | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
|  | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|     <meta name="description" content="Paperless-ngx Sign In"> | ||||
|     <meta name="author" content="Paperless-ngx project and contributors"> | ||||
|     <meta name="robots" content="noindex,nofollow"> | ||||
|  | ||||
|     <title>{% translate "Paperless-ngx social account sign up" %}</title> | ||||
|  | ||||
| 		<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> | ||||
|     <link href="{% static 'signin.css' %}" rel="stylesheet"> | ||||
|   </head> | ||||
|  | ||||
|   <body class="text-center"> | ||||
|     <div class="position-absolute top-50 start-50 translate-middle"> | ||||
|     <form class="form-signin" method="post" action="{% url 'socialaccount_signup' %}"> | ||||
| 			{% csrf_token %} | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" width="300" class="logo mb-4" viewBox="0 0 2897.4 896.6"> | ||||
|         <path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/> | ||||
|         <g class="text" style="fill:#000"> | ||||
|           <path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/> | ||||
|           <path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/> | ||||
|           <path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/> | ||||
|           <path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z"  transform="translate(0)"/> | ||||
|           <rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/> | ||||
|           <path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/> | ||||
|           <path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/> | ||||
|           <path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/> | ||||
|           <rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/> | ||||
|           <path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/> | ||||
|           <path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/> | ||||
|           <polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5  2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 	" transform="translate(0)"/> | ||||
|           <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/> | ||||
|         </g> | ||||
|       </svg> | ||||
|       <!-- TODO: Translations? --> | ||||
|       {% if form.errors.username %} | ||||
|         <div class="alert alert-danger" role="alert"> | ||||
|           {{ form.errors.username }} | ||||
|         </div> | ||||
|       {% endif %} | ||||
|       {% if form.errors.email %} | ||||
|         <div class="alert alert-danger" role="alert"> | ||||
|           {{ form.errors.email }} | ||||
|         </div> | ||||
|       {% endif %} | ||||
|         {% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your {{provider_name}} account to login to | ||||
| {{site_name}}. As a final step, please complete the following form:{% endblocktrans %} | ||||
|     </p> | ||||
|  			{% translate "Username" as i18n_username %} | ||||
|  			{% translate "Email" as i18n_email %} | ||||
|       <div class="form-floating"> | ||||
|         <input type="{{ form.username.type }}" name="{{ form.username.name }}" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus value="{{ form.username.value }}"> | ||||
|         <label for="inputUsername">{{ i18n_username }}</label> | ||||
|       </div> | ||||
|       <div class="form-floating"> | ||||
|         <input type="{{ form.email.type }}" name="{{ form.email.name }}" id="inputEmail" placeholder="{{ i18n_email }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus value="{{ form.email.value }}"> | ||||
|         <label for="inputEmail">{{ i18n_email }}</label> | ||||
|       </div> | ||||
|             {% if redirect_field_value %} | ||||
|                 <input type="hidden" | ||||
|                        name="{{ redirect_field_name }}" | ||||
|                        value="{{ redirect_field_value }}" /> | ||||
|             {% endif %} | ||||
|       <div class="d-grid mt-3"> | ||||
|         <button class="btn btn-lg btn-primary" type="submit">{% translate "Sign up" %}</button> | ||||
|       </div> | ||||
| 		</form> | ||||
|     </div> | ||||
| 	</body> | ||||
| </html> | ||||
| @@ -1,3 +1,7 @@ | ||||
| from unittest import mock | ||||
|  | ||||
| from allauth.socialaccount.models import SocialAccount | ||||
| from allauth.socialaccount.models import SocialApp | ||||
| from django.contrib.auth.models import User | ||||
| from rest_framework import status | ||||
| from rest_framework.authtoken.models import Token | ||||
| @@ -6,6 +10,44 @@ from rest_framework.test import APITestCase | ||||
| from documents.tests.utils import DirectoriesMixin | ||||
|  | ||||
|  | ||||
| # see allauth.socialaccount.providers.openid.provider.OpenIDProvider | ||||
| class MockOpenIDProvider: | ||||
|     id = "openid" | ||||
|     name = "OpenID" | ||||
|  | ||||
|     def get_brands(self): | ||||
|         default_servers = [ | ||||
|             dict(id="yahoo", name="Yahoo", openid_url="http://me.yahoo.com"), | ||||
|             dict(id="hyves", name="Hyves", openid_url="http://hyves.nl"), | ||||
|         ] | ||||
|         return default_servers | ||||
|  | ||||
|     def get_login_url(self, request, **kwargs): | ||||
|         return "openid/login/" | ||||
|  | ||||
|  | ||||
| # see allauth.socialaccount.providers.openid_connect.provider.OpenIDConnectProviderAccount | ||||
| class MockOpenIDConnectProviderAccount: | ||||
|     def __init__(self, mock_social_account_dict): | ||||
|         self.account = mock_social_account_dict | ||||
|  | ||||
|     def to_str(self): | ||||
|         return self.account["name"] | ||||
|  | ||||
|  | ||||
| # see allauth.socialaccount.providers.openid_connect.provider.OpenIDConnectProvider | ||||
| class MockOpenIDConnectProvider: | ||||
|     id = "openid_connect" | ||||
|     name = "OpenID Connect" | ||||
|  | ||||
|     def __init__(self, app=None): | ||||
|         self.app = app | ||||
|         self.name = app.name | ||||
|  | ||||
|     def get_login_url(self, request, **kwargs): | ||||
|         return f"{self.app.provider_id}/login/?process=connect" | ||||
|  | ||||
|  | ||||
| class TestApiProfile(DirectoriesMixin, APITestCase): | ||||
|     ENDPOINT = "/api/profile/" | ||||
|  | ||||
| @@ -19,6 +61,17 @@ class TestApiProfile(DirectoriesMixin, APITestCase): | ||||
|         ) | ||||
|         self.client.force_authenticate(user=self.user) | ||||
|  | ||||
|     def setupSocialAccount(self): | ||||
|         SocialApp.objects.create( | ||||
|             name="Keycloak", | ||||
|             provider="openid_connect", | ||||
|             provider_id="keycloak-test", | ||||
|         ) | ||||
|         self.user.socialaccount_set.add( | ||||
|             SocialAccount(uid="123456789", provider="keycloak-test"), | ||||
|             bulk=False, | ||||
|         ) | ||||
|  | ||||
|     def test_get_profile(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
| @@ -28,7 +81,6 @@ class TestApiProfile(DirectoriesMixin, APITestCase): | ||||
|         THEN: | ||||
|             - Profile is returned | ||||
|         """ | ||||
|  | ||||
|         response = self.client.get(self.ENDPOINT) | ||||
|  | ||||
|         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||
| @@ -37,6 +89,52 @@ class TestApiProfile(DirectoriesMixin, APITestCase): | ||||
|         self.assertEqual(response.data["first_name"], self.user.first_name) | ||||
|         self.assertEqual(response.data["last_name"], self.user.last_name) | ||||
|  | ||||
|     @mock.patch( | ||||
|         "allauth.socialaccount.models.SocialAccount.get_provider_account", | ||||
|     ) | ||||
|     @mock.patch( | ||||
|         "allauth.socialaccount.adapter.DefaultSocialAccountAdapter.list_providers", | ||||
|     ) | ||||
|     def test_get_profile_w_social(self, mock_list_providers, mock_get_provider_account): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Configured user and setup social account | ||||
|         WHEN: | ||||
|             - API call is made to get profile | ||||
|         THEN: | ||||
|             - Profile is returned with social accounts | ||||
|         """ | ||||
|         self.setupSocialAccount() | ||||
|  | ||||
|         openid_provider = ( | ||||
|             MockOpenIDConnectProvider( | ||||
|                 app=SocialApp.objects.get(provider_id="keycloak-test"), | ||||
|             ), | ||||
|         ) | ||||
|         mock_list_providers.return_value = [ | ||||
|             openid_provider, | ||||
|         ] | ||||
|         mock_get_provider_account.return_value = MockOpenIDConnectProviderAccount( | ||||
|             mock_social_account_dict={ | ||||
|                 "name": openid_provider[0].name, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         response = self.client.get(self.ENDPOINT) | ||||
|  | ||||
|         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||
|  | ||||
|         self.assertEqual( | ||||
|             response.data["social_accounts"], | ||||
|             [ | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "provider": "keycloak-test", | ||||
|                     "name": "Keycloak", | ||||
|                 }, | ||||
|             ], | ||||
|         ) | ||||
|  | ||||
|     def test_update_profile(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
| @@ -103,3 +201,101 @@ class TestApiProfile(DirectoriesMixin, APITestCase): | ||||
|  | ||||
|         response = self.client.post(f"{self.ENDPOINT}generate_auth_token/") | ||||
|         self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) | ||||
|  | ||||
|     @mock.patch( | ||||
|         "allauth.socialaccount.adapter.DefaultSocialAccountAdapter.list_providers", | ||||
|     ) | ||||
|     def test_get_social_account_providers( | ||||
|         self, | ||||
|         mock_list_providers, | ||||
|     ): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Configured user | ||||
|         WHEN: | ||||
|             - API call is made to get social account providers | ||||
|         THEN: | ||||
|             - Social account providers are returned | ||||
|         """ | ||||
|         self.setupSocialAccount() | ||||
|  | ||||
|         mock_list_providers.return_value = [ | ||||
|             MockOpenIDConnectProvider( | ||||
|                 app=SocialApp.objects.get(provider_id="keycloak-test"), | ||||
|             ), | ||||
|         ] | ||||
|  | ||||
|         response = self.client.get(f"{self.ENDPOINT}social_account_providers/") | ||||
|  | ||||
|         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||
|         self.assertEqual( | ||||
|             response.data[0]["name"], | ||||
|             "Keycloak", | ||||
|         ) | ||||
|         self.assertIn( | ||||
|             "keycloak-test/login/?process=connect", | ||||
|             response.data[0]["login_url"], | ||||
|         ) | ||||
|  | ||||
|     @mock.patch( | ||||
|         "allauth.socialaccount.adapter.DefaultSocialAccountAdapter.list_providers", | ||||
|     ) | ||||
|     def test_get_social_account_providers_openid( | ||||
|         self, | ||||
|         mock_list_providers, | ||||
|     ): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Configured user and openid social account provider | ||||
|         WHEN: | ||||
|             - API call is made to get social account providers | ||||
|         THEN: | ||||
|             - Brands for openid provider are returned | ||||
|         """ | ||||
|  | ||||
|         mock_list_providers.return_value = [ | ||||
|             MockOpenIDProvider(), | ||||
|         ] | ||||
|  | ||||
|         response = self.client.get(f"{self.ENDPOINT}social_account_providers/") | ||||
|  | ||||
|         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||
|         self.assertEqual( | ||||
|             len(response.data), | ||||
|             2, | ||||
|         ) | ||||
|  | ||||
|     def test_disconnect_social_account(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Configured user | ||||
|         WHEN: | ||||
|             - API call is made to disconnect a social account | ||||
|         THEN: | ||||
|             - Social account is deleted from the user or request fails | ||||
|         """ | ||||
|         self.setupSocialAccount() | ||||
|  | ||||
|         # Test with invalid id | ||||
|         response = self.client.post( | ||||
|             f"{self.ENDPOINT}disconnect_social_account/", | ||||
|             {"id": -1}, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|         # Test with valid id | ||||
|         social_account_id = self.user.socialaccount_set.all()[0].pk | ||||
|  | ||||
|         response = self.client.post( | ||||
|             f"{self.ENDPOINT}disconnect_social_account/", | ||||
|             {"id": social_account_id}, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||
|         self.assertEqual(response.data, social_account_id) | ||||
|  | ||||
|         self.assertEqual( | ||||
|             len(self.user.socialaccount_set.filter(pk=social_account_id)), | ||||
|             0, | ||||
|         ) | ||||
|   | ||||
| @@ -177,9 +177,9 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||
|             os.path.join(self.dirs.media_dir, "documents"), | ||||
|         ) | ||||
|  | ||||
|         manifest = self._do_export(use_filename_format=use_filename_format) | ||||
|         num_permission_objects = Permission.objects.count() | ||||
|  | ||||
|         self.assertEqual(len(manifest), 190) | ||||
|         manifest = self._do_export(use_filename_format=use_filename_format) | ||||
|  | ||||
|         # dont include consumer or AnonymousUser users | ||||
|         self.assertEqual( | ||||
| @@ -273,7 +273,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||
|             self.assertEqual(Document.objects.get(id=self.d4.id).title, "wow_dec") | ||||
|             self.assertEqual(GroupObjectPermission.objects.count(), 1) | ||||
|             self.assertEqual(UserObjectPermission.objects.count(), 1) | ||||
|             self.assertEqual(Permission.objects.count(), 136) | ||||
|             self.assertEqual(Permission.objects.count(), num_permission_objects) | ||||
|             messages = check_sanity() | ||||
|             # everything is alright after the test | ||||
|             self.assertEqual(len(messages), 0) | ||||
| @@ -753,15 +753,15 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||
|             os.path.join(self.dirs.media_dir, "documents"), | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(ContentType.objects.count(), 34) | ||||
|         self.assertEqual(Permission.objects.count(), 136) | ||||
|         num_content_type_objects = ContentType.objects.count() | ||||
|         num_permission_objects = Permission.objects.count() | ||||
|  | ||||
|         manifest = self._do_export() | ||||
|  | ||||
|         with paperless_environment(): | ||||
|             self.assertEqual( | ||||
|                 len(list(filter(lambda e: e["model"] == "auth.permission", manifest))), | ||||
|                 136, | ||||
|                 num_permission_objects, | ||||
|             ) | ||||
|             # add 1 more to db to show objects are not re-created by import | ||||
|             Permission.objects.create( | ||||
| @@ -769,7 +769,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||
|                 codename="test_perm", | ||||
|                 content_type_id=1, | ||||
|             ) | ||||
|             self.assertEqual(Permission.objects.count(), 137) | ||||
|             self.assertEqual(Permission.objects.count(), num_permission_objects + 1) | ||||
|  | ||||
|             # will cause an import error | ||||
|             self.user.delete() | ||||
| @@ -778,5 +778,5 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||
|             with self.assertRaises(IntegrityError): | ||||
|                 call_command("document_importer", "--no-progress-bar", self.target) | ||||
|  | ||||
|             self.assertEqual(ContentType.objects.count(), 34) | ||||
|             self.assertEqual(Permission.objects.count(), 137) | ||||
|             self.assertEqual(ContentType.objects.count(), num_content_type_objects) | ||||
|             self.assertEqual(Permission.objects.count(), num_permission_objects + 1) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: paperless-ngx\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2024-02-02 20:17-0800\n" | ||||
| "POT-Creation-Date: 2024-02-07 06:20+0000\n" | ||||
| "PO-Revision-Date: 2022-02-17 04:17\n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: English\n" | ||||
| @@ -777,15 +777,136 @@ msgstr "" | ||||
| msgid "Invalid color." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:1049 | ||||
| #: documents/serialisers.py:1060 | ||||
| #, python-format | ||||
| msgid "File type %(type)s not supported" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:1152 | ||||
| #: documents/serialisers.py:1163 | ||||
| msgid "Invalid variable detected." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:14 | ||||
| msgid "Paperless-ngx sign in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:47 | ||||
| msgid "Please sign in." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:50 | ||||
| msgid "Your username and password didn't match. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:54 | ||||
| msgid "Share link was not found." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:58 | ||||
| msgid "Share link has expired." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:61 | ||||
| #: documents/templates/socialaccount/signup.html:56 | ||||
| msgid "Username" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:62 | ||||
| msgid "Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:72 | ||||
| msgid "Sign in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:76 | ||||
| msgid "Forgot your password?" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:83 | ||||
| msgid "or sign in via" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset.html:15 | ||||
| msgid "Paperless-ngx reset password request" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset.html:43 | ||||
| msgid "" | ||||
| "Enter your email address below, and we'll email instructions for setting a " | ||||
| "new one." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset.html:46 | ||||
| msgid "An error occurred. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset.html:49 | ||||
| #: documents/templates/socialaccount/signup.html:57 | ||||
| msgid "Email" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset.html:56 | ||||
| msgid "Send me instructions!" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_done.html:14 | ||||
| msgid "Paperless-ngx reset password sent" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_done.html:40 | ||||
| msgid "Check your inbox." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_done.html:41 | ||||
| msgid "" | ||||
| "We've emailed you instructions for setting your password. You should receive " | ||||
| "the email shortly!" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:15 | ||||
| msgid "Paperless-ngx reset password confirmation" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:44 | ||||
| msgid "request a new password reset" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:46 | ||||
| msgid "Set a new password." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:50 | ||||
| msgid "Passwords did not match or too weak. Try again." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:53 | ||||
| msgid "New Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:54 | ||||
| msgid "Confirm Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key.html:65 | ||||
| msgid "Change my password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key_done.html:14 | ||||
| msgid "Paperless-ngx reset password complete" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key_done.html:40 | ||||
| msgid "Password reset complete." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/password_reset_from_key_done.html:42 | ||||
| #, python-format | ||||
| msgid "" | ||||
| "Your new password has been set. You can now <a href=\"%(login_url)s\">log " | ||||
| "in</a>" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/index.html:79 | ||||
| msgid "Paperless-ngx is loading..." | ||||
| msgstr "" | ||||
| @@ -798,131 +919,40 @@ msgstr "" | ||||
| msgid "Here's a link to the docs." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/logged_out.html:14 | ||||
| msgid "Paperless-ngx signed out" | ||||
| #: documents/templates/socialaccount/authentication_error.html:15 | ||||
| #: documents/templates/socialaccount/login.html:15 | ||||
| msgid "Paperless-ngx social account sign in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/logged_out.html:40 | ||||
| msgid "You have been successfully logged out. Bye!" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/logged_out.html:41 | ||||
| msgid "Sign in again" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:14 | ||||
| msgid "Paperless-ngx sign in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:41 | ||||
| msgid "Please sign in." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:44 | ||||
| msgid "Your username and password didn't match. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:48 | ||||
| msgid "Share link was not found." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:52 | ||||
| msgid "Share link has expired." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:55 | ||||
| msgid "Username" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:56 | ||||
| msgid "Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:66 | ||||
| msgid "Sign in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/login.html:70 | ||||
| msgid "Forgot your password?" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_complete.html:14 | ||||
| msgid "Paperless-ngx reset password complete" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_complete.html:40 | ||||
| msgid "Password reset complete." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_complete.html:42 | ||||
| #: documents/templates/socialaccount/authentication_error.html:43 | ||||
| #, python-format | ||||
| msgid "" | ||||
| "Your new password has been set. You can now <a href=\"%(login_url)s\">log " | ||||
| "in</a>" | ||||
| "An error occurred while attempting to login via your social network account. " | ||||
| "Back to the <a href=\"%(login_url)s\">login page</a>" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:14 | ||||
| msgid "Paperless-ngx reset password confirmation" | ||||
| #: documents/templates/socialaccount/login.html:44 | ||||
| #, python-format | ||||
| msgid "You are about to connect a new third-party account from %(provider)s." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:42 | ||||
| msgid "Set a new password." | ||||
| #: documents/templates/socialaccount/login.html:47 | ||||
| msgid "Continue" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:46 | ||||
| msgid "Passwords did not match or too weak. Try again." | ||||
| #: documents/templates/socialaccount/signup.html:14 | ||||
| msgid "Paperless-ngx social account sign up" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:49 | ||||
| msgid "New Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:50 | ||||
| msgid "Confirm Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:61 | ||||
| msgid "Change my password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_confirm.html:65 | ||||
| msgid "request a new password reset" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_done.html:14 | ||||
| msgid "Paperless-ngx reset password sent" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_done.html:40 | ||||
| msgid "Check your inbox." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_done.html:41 | ||||
| #: documents/templates/socialaccount/signup.html:53 | ||||
| #, python-format | ||||
| msgid "" | ||||
| "We've emailed you instructions for setting your password. You should receive " | ||||
| "the email shortly!" | ||||
| "You are about to use your %(provider_name)s account to login to\n" | ||||
| "%(site_name)s. As a final step, please complete the following form:" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_form.html:14 | ||||
| msgid "Paperless-ngx reset password request" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_form.html:41 | ||||
| msgid "" | ||||
| "Enter your email address below, and we'll email instructions for setting a " | ||||
| "new one." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_form.html:44 | ||||
| msgid "An error occurred. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_form.html:47 | ||||
| msgid "Email" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/registration/password_reset_form.html:54 | ||||
| msgid "Send me instructions!" | ||||
| #: documents/templates/socialaccount/signup.html:72 | ||||
| msgid "Sign up" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/validators.py:17 | ||||
| @@ -1088,135 +1118,135 @@ msgstr "" | ||||
| msgid "paperless application settings" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:617 | ||||
| #: paperless/settings.py:658 | ||||
| msgid "English (US)" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:618 | ||||
| #: paperless/settings.py:659 | ||||
| msgid "Arabic" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:619 | ||||
| #: paperless/settings.py:660 | ||||
| msgid "Afrikaans" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:620 | ||||
| #: paperless/settings.py:661 | ||||
| msgid "Belarusian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:621 | ||||
| #: paperless/settings.py:662 | ||||
| msgid "Bulgarian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:622 | ||||
| #: paperless/settings.py:663 | ||||
| msgid "Catalan" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:623 | ||||
| #: paperless/settings.py:664 | ||||
| msgid "Czech" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:624 | ||||
| #: paperless/settings.py:665 | ||||
| msgid "Danish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:625 | ||||
| #: paperless/settings.py:666 | ||||
| msgid "German" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:626 | ||||
| #: paperless/settings.py:667 | ||||
| msgid "Greek" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:627 | ||||
| #: paperless/settings.py:668 | ||||
| msgid "English (GB)" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:628 | ||||
| #: paperless/settings.py:669 | ||||
| msgid "Spanish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:629 | ||||
| #: paperless/settings.py:670 | ||||
| msgid "Finnish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:630 | ||||
| #: paperless/settings.py:671 | ||||
| msgid "French" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:631 | ||||
| #: paperless/settings.py:672 | ||||
| msgid "Hungarian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:632 | ||||
| #: paperless/settings.py:673 | ||||
| msgid "Italian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:633 | ||||
| #: paperless/settings.py:674 | ||||
| msgid "Japanese" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:634 | ||||
| #: paperless/settings.py:675 | ||||
| msgid "Luxembourgish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:635 | ||||
| #: paperless/settings.py:676 | ||||
| msgid "Norwegian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:636 | ||||
| #: paperless/settings.py:677 | ||||
| msgid "Dutch" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:637 | ||||
| #: paperless/settings.py:678 | ||||
| msgid "Polish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:638 | ||||
| #: paperless/settings.py:679 | ||||
| msgid "Portuguese (Brazil)" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:639 | ||||
| #: paperless/settings.py:680 | ||||
| msgid "Portuguese" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:640 | ||||
| #: paperless/settings.py:681 | ||||
| msgid "Romanian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:641 | ||||
| #: paperless/settings.py:682 | ||||
| msgid "Russian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:642 | ||||
| #: paperless/settings.py:683 | ||||
| msgid "Slovak" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:643 | ||||
| #: paperless/settings.py:684 | ||||
| msgid "Slovenian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:644 | ||||
| #: paperless/settings.py:685 | ||||
| msgid "Serbian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:645 | ||||
| #: paperless/settings.py:686 | ||||
| msgid "Swedish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:646 | ||||
| #: paperless/settings.py:687 | ||||
| msgid "Turkish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:647 | ||||
| #: paperless/settings.py:688 | ||||
| msgid "Ukrainian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:648 | ||||
| #: paperless/settings.py:689 | ||||
| msgid "Chinese Simplified" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/urls.py:214 | ||||
| #: paperless/urls.py:224 | ||||
| msgid "Paperless-ngx administration" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
							
								
								
									
										30
									
								
								src/paperless/adapter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/paperless/adapter.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| from allauth.account.adapter import DefaultAccountAdapter | ||||
| from allauth.socialaccount.adapter import DefaultSocialAccountAdapter | ||||
| from django.conf import settings | ||||
| from django.urls import reverse | ||||
|  | ||||
|  | ||||
| class CustomAccountAdapter(DefaultAccountAdapter): | ||||
|     def is_open_for_signup(self, request): | ||||
|         allow_signups = super().is_open_for_signup(request) | ||||
|         # Override with setting, otherwise default to super. | ||||
|         return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups) | ||||
|  | ||||
|  | ||||
| class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): | ||||
|     def is_open_for_signup(self, request, sociallogin): | ||||
|         allow_signups = super().is_open_for_signup(request, sociallogin) | ||||
|         # Override with setting, otherwise default to super. | ||||
|         return getattr(settings, "SOCIALACCOUNT_ALLOW_SIGNUPS", allow_signups) | ||||
|  | ||||
|     def get_connect_redirect_url(self, request, socialaccount): | ||||
|         """ | ||||
|         Returns the default URL to redirect to after successfully | ||||
|         connecting a social account. | ||||
|         """ | ||||
|         url = reverse("base") | ||||
|         return url | ||||
|  | ||||
|     def populate_user(self, request, sociallogin, data): | ||||
|         # TODO: If default global permissions are implemented, should also be here | ||||
|         return super().populate_user(request, sociallogin, data)  # pragma: no cover | ||||
| @@ -1,5 +1,6 @@ | ||||
| import logging | ||||
|  | ||||
| from allauth.socialaccount.models import SocialAccount | ||||
| from django.contrib.auth.models import Group | ||||
| from django.contrib.auth.models import Permission | ||||
| from django.contrib.auth.models import User | ||||
| @@ -105,10 +106,30 @@ class GroupSerializer(serializers.ModelSerializer): | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class SocialAccountSerializer(serializers.ModelSerializer): | ||||
|     name = serializers.SerializerMethodField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = SocialAccount | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "provider", | ||||
|             "name", | ||||
|         ) | ||||
|  | ||||
|     def get_name(self, obj): | ||||
|         return obj.get_provider_account().to_str() | ||||
|  | ||||
|  | ||||
| class ProfileSerializer(serializers.ModelSerializer): | ||||
|     email = serializers.EmailField(allow_null=False) | ||||
|     password = ObfuscatedUserPasswordField(required=False, allow_null=False) | ||||
|     auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key") | ||||
|     social_accounts = SocialAccountSerializer( | ||||
|         many=True, | ||||
|         read_only=True, | ||||
|         source="socialaccount_set", | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         model = User | ||||
| @@ -118,6 +139,8 @@ class ProfileSerializer(serializers.ModelSerializer): | ||||
|             "first_name", | ||||
|             "last_name", | ||||
|             "auth_token", | ||||
|             "social_accounts", | ||||
|             "has_usable_password", | ||||
|         ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -303,6 +303,9 @@ INSTALLED_APPS = [ | ||||
|     "django_filters", | ||||
|     "django_celery_results", | ||||
|     "guardian", | ||||
|     "allauth", | ||||
|     "allauth.account", | ||||
|     "allauth.socialaccount", | ||||
|     *env_apps, | ||||
| ] | ||||
|  | ||||
| @@ -339,6 +342,7 @@ MIDDLEWARE = [ | ||||
|     "django.contrib.auth.middleware.AuthenticationMiddleware", | ||||
|     "django.contrib.messages.middleware.MessageMiddleware", | ||||
|     "django.middleware.clickjacking.XFrameOptionsMiddleware", | ||||
|     "allauth.account.middleware.AccountMiddleware", | ||||
| ] | ||||
|  | ||||
| # Optional to enable compression | ||||
| @@ -350,6 +354,7 @@ ROOT_URLCONF = "paperless.urls" | ||||
| FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME") | ||||
| BASE_URL = (FORCE_SCRIPT_NAME or "") + "/" | ||||
| LOGIN_URL = BASE_URL + "accounts/login/" | ||||
| LOGIN_REDIRECT_URL = "/dashboard" | ||||
| LOGOUT_REDIRECT_URL = os.getenv("PAPERLESS_LOGOUT_REDIRECT_URL") | ||||
|  | ||||
| WSGI_APPLICATION = "paperless.wsgi.application" | ||||
| @@ -410,8 +415,28 @@ CHANNEL_LAYERS = { | ||||
| AUTHENTICATION_BACKENDS = [ | ||||
|     "guardian.backends.ObjectPermissionBackend", | ||||
|     "django.contrib.auth.backends.ModelBackend", | ||||
|     "allauth.account.auth_backends.AuthenticationBackend", | ||||
| ] | ||||
|  | ||||
| ACCOUNT_LOGOUT_ON_GET = True | ||||
| ACCOUNT_DEFAULT_HTTP_PROTOCOL = os.getenv( | ||||
|     "PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL", | ||||
|     "https", | ||||
| ) | ||||
|  | ||||
| ACCOUNT_ADAPTER = "paperless.adapter.CustomAccountAdapter" | ||||
| ACCOUNT_ALLOW_SIGNUPS = __get_boolean("PAPERLESS_ACCOUNT_ALLOW_SIGNUPS") | ||||
|  | ||||
| SOCIALACCOUNT_ADAPTER = "paperless.adapter.CustomSocialAccountAdapter" | ||||
| SOCIALACCOUNT_ALLOW_SIGNUPS = __get_boolean( | ||||
|     "PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS", | ||||
|     "yes", | ||||
| ) | ||||
| SOCIALACCOUNT_AUTO_SIGNUP = __get_boolean("PAPERLESS_SOCIAL_AUTO_SIGNUP") | ||||
| SOCIALACCOUNT_PROVIDERS = json.loads( | ||||
|     os.getenv("PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"), | ||||
| ) | ||||
|  | ||||
| AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME") | ||||
|  | ||||
| if AUTO_LOGIN_USERNAME: | ||||
|   | ||||
							
								
								
									
										43
									
								
								src/paperless/tests/test_adapter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/paperless/tests/test_adapter.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| from allauth.account.adapter import get_adapter | ||||
| from allauth.socialaccount.adapter import get_adapter as get_social_adapter | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
|  | ||||
|  | ||||
| class TestCustomAccountAdapter(TestCase): | ||||
|     def test_is_open_for_signup(self): | ||||
|         adapter = get_adapter() | ||||
|  | ||||
|         # Test when ACCOUNT_ALLOW_SIGNUPS is True | ||||
|         settings.ACCOUNT_ALLOW_SIGNUPS = True | ||||
|         self.assertTrue(adapter.is_open_for_signup(None)) | ||||
|  | ||||
|         # Test when ACCOUNT_ALLOW_SIGNUPS is False | ||||
|         settings.ACCOUNT_ALLOW_SIGNUPS = False | ||||
|         self.assertFalse(adapter.is_open_for_signup(None)) | ||||
|  | ||||
|  | ||||
| class TestCustomSocialAccountAdapter(TestCase): | ||||
|     def test_is_open_for_signup(self): | ||||
|         adapter = get_social_adapter() | ||||
|  | ||||
|         # Test when SOCIALACCOUNT_ALLOW_SIGNUPS is True | ||||
|         settings.SOCIALACCOUNT_ALLOW_SIGNUPS = True | ||||
|         self.assertTrue(adapter.is_open_for_signup(None, None)) | ||||
|  | ||||
|         # Test when SOCIALACCOUNT_ALLOW_SIGNUPS is False | ||||
|         settings.SOCIALACCOUNT_ALLOW_SIGNUPS = False | ||||
|         self.assertFalse(adapter.is_open_for_signup(None, None)) | ||||
|  | ||||
|     def test_get_connect_redirect_url(self): | ||||
|         adapter = get_social_adapter() | ||||
|         request = None | ||||
|         socialaccount = None | ||||
|  | ||||
|         # Test the default URL | ||||
|         expected_url = reverse("base") | ||||
|         self.assertEqual( | ||||
|             adapter.get_connect_redirect_url(request, socialaccount), | ||||
|             expected_url, | ||||
|         ) | ||||
| @@ -41,10 +41,12 @@ from documents.views import WorkflowTriggerViewSet | ||||
| from documents.views import WorkflowViewSet | ||||
| from paperless.consumers import StatusConsumer | ||||
| from paperless.views import ApplicationConfigurationViewSet | ||||
| from paperless.views import DisconnectSocialAccountView | ||||
| from paperless.views import FaviconView | ||||
| from paperless.views import GenerateAuthTokenView | ||||
| from paperless.views import GroupViewSet | ||||
| from paperless.views import ProfileView | ||||
| from paperless.views import SocialAccountProvidersView | ||||
| from paperless.views import UserViewSet | ||||
| from paperless_mail.views import MailAccountTestView | ||||
| from paperless_mail.views import MailAccountViewSet | ||||
| @@ -132,6 +134,14 @@ urlpatterns = [ | ||||
|                     name="bulk_edit_object_permissions", | ||||
|                 ), | ||||
|                 path("profile/generate_auth_token/", GenerateAuthTokenView.as_view()), | ||||
|                 path( | ||||
|                     "profile/disconnect_social_account/", | ||||
|                     DisconnectSocialAccountView.as_view(), | ||||
|                 ), | ||||
|                 path( | ||||
|                     "profile/social_account_providers/", | ||||
|                     SocialAccountProvidersView.as_view(), | ||||
|                 ), | ||||
|                 re_path( | ||||
|                     "^profile/", | ||||
|                     ProfileView.as_view(), | ||||
| @@ -192,7 +202,7 @@ urlpatterns = [ | ||||
|     ), | ||||
|     # TODO: with localization, this is even worse! :/ | ||||
|     # login, logout | ||||
|     path("accounts/", include("django.contrib.auth.urls")), | ||||
|     path("accounts/", include("allauth.urls")), | ||||
|     # Root of the Frontend | ||||
|     re_path( | ||||
|         r".*", | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| import os | ||||
| from collections import OrderedDict | ||||
|  | ||||
| from allauth.socialaccount.adapter import get_adapter | ||||
| from allauth.socialaccount.models import SocialAccount | ||||
| from django.contrib.auth.models import Group | ||||
| from django.contrib.auth.models import User | ||||
| from django.db.models.functions import Lower | ||||
| from django.http import HttpResponse | ||||
| from django.http import HttpResponseBadRequest | ||||
| from django.views.generic import View | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework.authtoken.models import Token | ||||
| @@ -14,6 +17,7 @@ from rest_framework.pagination import PageNumberPagination | ||||
| from rest_framework.permissions import DjangoObjectPermissions | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from documents.permissions import PaperlessObjectPermissions | ||||
| @@ -168,3 +172,54 @@ class ApplicationConfigurationViewSet(ModelViewSet): | ||||
|  | ||||
|     serializer_class = ApplicationConfigurationSerializer | ||||
|     permission_classes = (IsAuthenticated, DjangoObjectPermissions) | ||||
|  | ||||
|  | ||||
| class DisconnectSocialAccountView(GenericAPIView): | ||||
|     """ | ||||
|     Disconnects a social account provider from the user account | ||||
|     """ | ||||
|  | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         user = self.request.user | ||||
|  | ||||
|         try: | ||||
|             account = user.socialaccount_set.get(pk=request.data["id"]) | ||||
|             account_id = account.id | ||||
|             account.delete() | ||||
|             return Response(account_id) | ||||
|         except SocialAccount.DoesNotExist: | ||||
|             return HttpResponseBadRequest("Social account not found") | ||||
|  | ||||
|  | ||||
| class SocialAccountProvidersView(APIView): | ||||
|     """ | ||||
|     List of social account providers | ||||
|     """ | ||||
|  | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         adapter = get_adapter() | ||||
|         providers = adapter.list_providers(request) | ||||
|         resp = [ | ||||
|             {"name": p.name, "login_url": p.get_login_url(request, process="connect")} | ||||
|             for p in providers | ||||
|             if p.id != "openid" | ||||
|         ] | ||||
|  | ||||
|         for openid_provider in filter(lambda p: p.id == "openid", providers): | ||||
|             resp += [ | ||||
|                 { | ||||
|                     "name": b["name"], | ||||
|                     "login_url": openid_provider.get_login_url( | ||||
|                         request, | ||||
|                         process="connect", | ||||
|                         openid=b["openid_url"], | ||||
|                     ), | ||||
|                 } | ||||
|                 for b in openid_provider.get_brands() | ||||
|             ] | ||||
|  | ||||
|         return Response(sorted(resp, key=lambda p: p["name"])) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Moritz Pflanzer
					Moritz Pflanzer