TokenLifeTime ve RefreshTokenLifeTime tek yerden yönetilir.
This commit is contained in:
parent
76621b24d8
commit
cb6b38f141
15 changed files with 125 additions and 58 deletions
|
|
@ -19,9 +19,7 @@
|
|||
},
|
||||
"Platform_App": {
|
||||
"ClientId": "Platform_App",
|
||||
"RootUrl": "https://dev.sozsoft.com",
|
||||
"TokenLifeTime": 2,
|
||||
"RefreshTokenLifeTime": 8760
|
||||
"RootUrl": "https://dev.sozsoft.com"
|
||||
},
|
||||
"Platform_Swagger": {
|
||||
"ClientId": "Platform_Swagger",
|
||||
|
|
@ -29,9 +27,7 @@
|
|||
},
|
||||
"Platform_PublicApi": {
|
||||
"ClientId": "Platform_PublicApi",
|
||||
"RootUrl": "https://dev.sozsoft.com",
|
||||
"TokenLifeTime": 2,
|
||||
"RefreshTokenLifeTime": 8760
|
||||
"RootUrl": "https://dev.sozsoft.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@
|
|||
},
|
||||
"Platform_App": {
|
||||
"ClientId": "Platform_App",
|
||||
"RootUrl": "https://sozsoft.com",
|
||||
"TokenLifeTime": 2,
|
||||
"RefreshTokenLifeTime": 8760
|
||||
"RootUrl": "https://sozsoft.com"
|
||||
},
|
||||
"Platform_Swagger": {
|
||||
"ClientId": "Platform_Swagger",
|
||||
|
|
@ -29,9 +27,7 @@
|
|||
},
|
||||
"Platform_PublicApi": {
|
||||
"ClientId": "Platform_PublicApi",
|
||||
"RootUrl": "https://sozsoft.com",
|
||||
"TokenLifeTime": 2,
|
||||
"RefreshTokenLifeTime": 8760
|
||||
"RootUrl": "https://sozsoft.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@
|
|||
},
|
||||
"Platform_App": {
|
||||
"ClientId": "Platform_App",
|
||||
"RootUrl": "http://localhost:3000",
|
||||
"TokenLifeTime": 2,
|
||||
"RefreshTokenLifeTime": 8760
|
||||
"RootUrl": "http://localhost:3000"
|
||||
},
|
||||
"Platform_Swagger": {
|
||||
"ClientId": "Platform_Swagger",
|
||||
|
|
@ -30,9 +28,7 @@
|
|||
},
|
||||
"Platform_PublicApi": {
|
||||
"ClientId": "Platform_PublicApi",
|
||||
"RootUrl": "http://localhost:3000",
|
||||
"TokenLifeTime": 2,
|
||||
"RefreshTokenLifeTime": 8760
|
||||
"RootUrl": "http://localhost:3000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||
using Volo.Abp.Uow;
|
||||
using Volo.Abp.Data;
|
||||
using System.Text.Json;
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using System.Threading.Tasks;
|
||||
using OpenIddict.Abstractions;
|
||||
|
|
@ -108,9 +107,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
|
|||
scopes: commonScopes,
|
||||
redirectUri: consoleAndAngularClientRootUrl,
|
||||
clientUri: consoleAndAngularClientRootUrl,
|
||||
postLogoutRedirectUri: consoleAndAngularClientRootUrl,
|
||||
tokenLifeTime: configurationSection["Platform_App:TokenLifeTime"]?.To<int>(),
|
||||
refreshTokenLifeTime: configurationSection["Platform_App:RefreshTokenLifeTime"]?.To<int>()
|
||||
postLogoutRedirectUri: consoleAndAngularClientRootUrl
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -132,9 +129,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
|
|||
},
|
||||
scopes: commonScopes,
|
||||
redirectUri: $"{swaggerRootUrl}/swagger/oauth2-redirect.html",
|
||||
clientUri: swaggerRootUrl,
|
||||
tokenLifeTime: configurationSection["Platform_Swagger:TokenLifeTime"]?.To<int>(),
|
||||
refreshTokenLifeTime: configurationSection["Platform_Swagger:RefreshTokenLifeTime"]?.To<int>()
|
||||
clientUri: swaggerRootUrl
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -157,9 +152,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
|
|||
scopes: commonScopes,
|
||||
redirectUri: publicApiClientRootUrl,
|
||||
clientUri: publicApiClientRootUrl,
|
||||
postLogoutRedirectUri: publicApiClientRootUrl,
|
||||
tokenLifeTime: configurationSection["Platform_PublicApi:TokenLifeTime"]?.To<int>(),
|
||||
refreshTokenLifeTime: configurationSection["Platform_PublicApi:RefreshTokenLifeTime"]?.To<int>()
|
||||
postLogoutRedirectUri: publicApiClientRootUrl
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +169,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
|
|||
string? clientUri = null,
|
||||
string? redirectUri = null,
|
||||
string? postLogoutRedirectUri = null,
|
||||
List<string>? permissions = null,
|
||||
int? tokenLifeTime = 1,
|
||||
int? refreshTokenLifeTime = 336
|
||||
List<string>? permissions = null
|
||||
)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
|
||||
|
|
@ -200,13 +191,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
|
|||
ClientSecret = secret,
|
||||
ConsentType = consentType,
|
||||
DisplayName = displayName,
|
||||
ClientUri = clientUri,
|
||||
Settings = {
|
||||
[OpenIddictConstants.Settings.TokenLifetimes.AccessToken] =
|
||||
TimeSpan.FromHours(tokenLifeTime ?? 1).ToString("c", CultureInfo.InvariantCulture), // 1 saat
|
||||
[OpenIddictConstants.Settings.TokenLifetimes.RefreshToken] =
|
||||
TimeSpan.FromHours(refreshTokenLifeTime ?? 336).ToString("c", CultureInfo.InvariantCulture), // 336 saat = 14 gün
|
||||
}
|
||||
ClientUri = clientUri
|
||||
};
|
||||
|
||||
Check.NotNullOrEmpty(grantTypes, nameof(grantTypes));
|
||||
|
|
@ -382,6 +367,34 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
|
|||
client.Permissions = JsonSerializer.Serialize(application.Permissions.Select(q => q.ToString()));
|
||||
await _applicationManager.UpdateAsync(client.ToModel());
|
||||
}
|
||||
|
||||
await RemoveTokenLifetimeSettingsAsync(client);
|
||||
}
|
||||
|
||||
private async Task RemoveTokenLifetimeSettingsAsync(OpenIddictApplication client)
|
||||
{
|
||||
if (client.Settings.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = JsonSerializer.Deserialize<Dictionary<string, string>>(client.Settings);
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
changed |= settings.Remove(OpenIddictConstants.Settings.TokenLifetimes.AccessToken);
|
||||
changed |= settings.Remove(OpenIddictConstants.Settings.TokenLifetimes.RefreshToken);
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
client.Settings = settings.Count == 0 ? null : JsonSerializer.Serialize(settings);
|
||||
await _applicationManager.UpdateAsync(client.ToModel());
|
||||
}
|
||||
|
||||
private bool HasSameRedirectUris(OpenIddictApplication existingClient, AbpApplicationDescriptor application)
|
||||
|
|
|
|||
|
|
@ -83,12 +83,14 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
{
|
||||
public override void PreConfigureServices(ServiceConfigurationContext context)
|
||||
{
|
||||
var configuration = context.Services.GetConfiguration();
|
||||
|
||||
PreConfigure<OpenIddictBuilder>(builder =>
|
||||
{
|
||||
builder.AddServer(server =>
|
||||
{
|
||||
server.SetAccessTokenLifetime(TimeSpan.FromMinutes(60));
|
||||
server.SetRefreshTokenLifetime(TimeSpan.FromMinutes(90));
|
||||
server.SetAccessTokenLifetime(TimeSpan.FromMinutes(configuration.GetValue("OpenIddict:TokenLifetimes:AccessTokenMinutes", 60)));
|
||||
server.SetRefreshTokenLifetime(TimeSpan.FromMinutes(configuration.GetValue("OpenIddict:TokenLifetimes:RefreshTokenMinutes", 90)));
|
||||
server.AddEventHandler<HandleRevocationRequestContext>(builder =>
|
||||
builder.UseScopedHandler<PlatformSessionRevocationHandler>());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ public class DynamicFormReport : XtraReport
|
|||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
|
||||
var isSubReport = DynamicReportImageHelper.IsSubReport(parameters);
|
||||
|
||||
if (!await authManager.CanAccess(listFormCode))
|
||||
{
|
||||
|
|
@ -119,7 +120,7 @@ public class DynamicFormReport : XtraReport
|
|||
|
||||
ConfigurePage();
|
||||
ConfigureDataSource(selectQuery, connectionString, dataSourceType);
|
||||
await BuildLayoutAsync(listForm, reportFields, row, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName);
|
||||
await BuildLayoutAsync(listForm, reportFields, row, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName, isSubReport);
|
||||
RequestParameters = false;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +159,8 @@ public class DynamicFormReport : XtraReport
|
|||
IDictionary<string, object> row,
|
||||
IStringLocalizer<PlatformResource> localizer,
|
||||
string imageBaseUrl,
|
||||
string currentUserName)
|
||||
string currentUserName,
|
||||
bool isSubReport)
|
||||
{
|
||||
Bands.Clear();
|
||||
var formGroups = GetFormGroups(listForm, fields).ToList();
|
||||
|
|
@ -179,7 +181,10 @@ public class DynamicFormReport : XtraReport
|
|||
Font = new DXFont("Arial", 14F, DXFontStyle.Bold),
|
||||
TextAlignment = TextAlignment.MiddleLeft
|
||||
});
|
||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||
if (!isSubReport)
|
||||
{
|
||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||
}
|
||||
|
||||
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
||||
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
||||
|
|
@ -667,7 +672,8 @@ public class DynamicFormReport : XtraReport
|
|||
return new Dictionary<string, string>
|
||||
{
|
||||
["listFormCode"] = subForm.Code,
|
||||
["filter"] = filter
|
||||
["filter"] = filter,
|
||||
[DynamicReportImageHelper.IsSubReportParameterName] = bool.TrueString
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ public class DynamicGridReport : XtraReport
|
|||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
|
||||
var isSubReport = DynamicReportImageHelper.IsSubReport(parameters);
|
||||
|
||||
if (!await authManager.CanAccess(listFormCode))
|
||||
{
|
||||
|
|
@ -156,7 +157,7 @@ public class DynamicGridReport : XtraReport
|
|||
selectQuery,
|
||||
connectionString,
|
||||
dataSourceType);
|
||||
BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText);
|
||||
BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport);
|
||||
RequestParameters = false;
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +246,8 @@ public class DynamicGridReport : XtraReport
|
|||
IStringLocalizer<PlatformResource> localizer,
|
||||
string imageBaseUrl,
|
||||
string currentUserName,
|
||||
string criteriaText)
|
||||
string criteriaText,
|
||||
bool isSubReport)
|
||||
{
|
||||
Bands.Clear();
|
||||
|
||||
|
|
@ -271,7 +273,10 @@ public class DynamicGridReport : XtraReport
|
|||
Font = new DXFont("Arial", 14F, DXFontStyle.Bold),
|
||||
TextAlignment = TextAlignment.MiddleLeft
|
||||
});
|
||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||
if (!isSubReport)
|
||||
{
|
||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||
}
|
||||
DynamicReportImageHelper.AddReportCriteriaToHeader(reportHeader.Controls, criteriaText, pageWidth);
|
||||
|
||||
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace Sozsoft.Reports.PredefinedReports;
|
|||
|
||||
internal static class DynamicReportImageHelper
|
||||
{
|
||||
public const string IsSubReportParameterName = "isSubReport";
|
||||
private const float DefaultGridImageHeight = 82F;
|
||||
private const float DefaultGridImageWidth = 86F;
|
||||
private const float DefaultFormImageHeight = 86F;
|
||||
|
|
@ -394,6 +395,13 @@ internal static class DynamicReportImageHelper
|
|||
return parameters.TryGetValue(name, out var value) ? value : null;
|
||||
}
|
||||
|
||||
public static bool IsSubReport(IDictionary<string, string> parameters)
|
||||
{
|
||||
var value = GetParameter(parameters, IsSubReportParameterName);
|
||||
return string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(value, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetImageBaseUrl(IConfiguration configuration)
|
||||
{
|
||||
return configuration["App:CdnUrl"] ?? configuration["App:SelfUrl"];
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public class DynamicTreeReport : XtraReport
|
|||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
|
||||
var isSubReport = DynamicReportImageHelper.IsSubReport(parameters);
|
||||
|
||||
if (!await authManager.CanAccess(listFormCode))
|
||||
{
|
||||
|
|
@ -166,7 +167,7 @@ public class DynamicTreeReport : XtraReport
|
|||
|
||||
ConfigurePage(reportColumns);
|
||||
ConfigureDataSource(orderedRows, reportColumns);
|
||||
BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText);
|
||||
BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport);
|
||||
RequestParameters = false;
|
||||
}
|
||||
|
||||
|
|
@ -417,7 +418,8 @@ public class DynamicTreeReport : XtraReport
|
|||
IStringLocalizer<PlatformResource> localizer,
|
||||
string imageBaseUrl,
|
||||
string currentUserName,
|
||||
string criteriaText)
|
||||
string criteriaText,
|
||||
bool isSubReport)
|
||||
{
|
||||
Bands.Clear();
|
||||
|
||||
|
|
@ -443,7 +445,10 @@ public class DynamicTreeReport : XtraReport
|
|||
Font = new DXFont("Arial", 14F, DXFontStyle.Bold),
|
||||
TextAlignment = TextAlignment.MiddleLeft
|
||||
});
|
||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||
if (!isSubReport)
|
||||
{
|
||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||
}
|
||||
DynamicReportImageHelper.AddReportCriteriaToHeader(reportHeader.Controls, criteriaText, pageWidth);
|
||||
|
||||
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@
|
|||
"RequireHttpsMetadata": false,
|
||||
"SwaggerClientId": "Platform_Swagger"
|
||||
},
|
||||
"OpenIddict": {
|
||||
"TokenLifetimes": {
|
||||
"AccessTokenMinutes": 60,
|
||||
"RefreshTokenMinutes": 90
|
||||
}
|
||||
},
|
||||
"StringEncryption": {
|
||||
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@
|
|||
"RequireHttpsMetadata": false,
|
||||
"SwaggerClientId": "Platform_Swagger"
|
||||
},
|
||||
"OpenIddict": {
|
||||
"TokenLifetimes": {
|
||||
"AccessTokenMinutes": 60,
|
||||
"RefreshTokenMinutes": 90
|
||||
}
|
||||
},
|
||||
"StringEncryption": {
|
||||
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"RedirectAllowedUrls": "http://localhost:4200,http://localhost:4200/authentication/callback",
|
||||
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\mail-queue\\attachments",
|
||||
"CdnUrl": "http://localhost:4005",
|
||||
"CdnPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\docker\\data\\cdn",
|
||||
"CdnPath": "D:\\Sozsoft\\sozsoft-platform\\configs\\docker\\data\\cdn",
|
||||
"Version": "1.0.5",
|
||||
"BackupPath": "/var/opt/mssql/backup"
|
||||
},
|
||||
|
|
@ -23,6 +23,12 @@
|
|||
"RequireHttpsMetadata": false,
|
||||
"SwaggerClientId": "Platform_Swagger"
|
||||
},
|
||||
"OpenIddict": {
|
||||
"TokenLifetimes": {
|
||||
"AccessTokenMinutes": 60,
|
||||
"RefreshTokenMinutes": 90
|
||||
}
|
||||
},
|
||||
"StringEncryption": {
|
||||
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -47,17 +47,31 @@ export const refreshToken = (refresh_token: string) =>
|
|||
authConfig,
|
||||
)
|
||||
|
||||
export const signOut = (data: any) =>
|
||||
const revokeToken = (token: string, tokenTypeHint: 'access_token' | 'refresh_token') =>
|
||||
apiService.fetchData(
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/connect/revocat',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
data: new URLSearchParams({
|
||||
token: data.token,
|
||||
token,
|
||||
client_id: 'Platform_App',
|
||||
token_type_hint: 'access_token',
|
||||
token_type_hint: tokenTypeHint,
|
||||
}).toString(),
|
||||
},
|
||||
authConfig,
|
||||
)
|
||||
|
||||
export const signOut = (data: { token?: string; refreshToken?: string }) => {
|
||||
const requests = []
|
||||
|
||||
if (data.token) {
|
||||
requests.push(revokeToken(data.token, 'access_token'))
|
||||
}
|
||||
|
||||
if (data.refreshToken) {
|
||||
requests.push(revokeToken(data.refreshToken, 'refresh_token'))
|
||||
}
|
||||
|
||||
return Promise.all(requests)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import axios from 'axios'
|
|||
import { store } from '../store'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import appConfig from '../proxy/configs/app.config'
|
||||
import { refreshToken } from './auth.service'
|
||||
import { refreshToken, signOut as revokeTokens } from './auth.service'
|
||||
import { isLoginSuccess } from '../proxy/account/models'
|
||||
|
||||
const unauthorizedCode = [401]
|
||||
|
|
@ -103,11 +103,19 @@ platformApiService.interceptors.response.use(
|
|||
error.config.headers['Authorization'] = `Bearer ${access_token}`
|
||||
return axios.request(error.config)
|
||||
} else {
|
||||
await revokeTokens({
|
||||
token: auth.session.token,
|
||||
refreshToken: auth.session.refreshToken,
|
||||
}).catch(() => undefined)
|
||||
setIsRefreshing(false)
|
||||
signOut()
|
||||
error.silent = true
|
||||
}
|
||||
} catch (error2) {
|
||||
await revokeTokens({
|
||||
token: auth.session.token,
|
||||
refreshToken: auth.session.refreshToken,
|
||||
}).catch(() => undefined)
|
||||
setIsRefreshing(false)
|
||||
signOut()
|
||||
error.silent = true
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function useAuth() {
|
|||
|
||||
const query = useQuery()
|
||||
|
||||
const { token, signedIn } = useStoreState((state) => state.auth.session)
|
||||
const { token, refreshToken, signedIn } = useStoreState((state) => state.auth.session)
|
||||
|
||||
const handleToken = ({
|
||||
token,
|
||||
|
|
@ -135,7 +135,7 @@ function useAuth() {
|
|||
}
|
||||
|
||||
const _signOut = async () => {
|
||||
await signOut({ token })
|
||||
await signOut({ token, refreshToken })
|
||||
signOutStore()
|
||||
navigate(appConfig.unAuthenticatedEntryPath)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue