log
This commit is contained in:
@@ -1,72 +1,72 @@
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
||||
import { magicLink } from 'better-auth/plugins'
|
||||
import { admin as adminPlugin } from 'better-auth/plugins'
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
import { sendMagicLinkEmail } from './email'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: 'postgresql',
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
trustedOrigins: [
|
||||
process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3010',
|
||||
process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3010',
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001',
|
||||
'http://localhost:3010',
|
||||
'http://localhost:8081',
|
||||
'http://*.localhost:3010',
|
||||
'http://*.localhost:3000',
|
||||
'https://*.innungsapp.de',
|
||||
'https://*.innungsapp.com',
|
||||
// Additional origins from env (comma-separated)
|
||||
...(process.env.BETTER_AUTH_TRUSTED_ORIGINS ?? '').split(',').map((o) => o.trim()).filter(Boolean),
|
||||
],
|
||||
user: {
|
||||
additionalFields: {
|
||||
mustChangePassword: {
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, url }) => {
|
||||
await sendMagicLinkEmail({ to: email, magicUrl: url })
|
||||
},
|
||||
}),
|
||||
adminPlugin(),
|
||||
],
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type Auth = typeof auth
|
||||
|
||||
export async function getSanitizedHeaders(sourceHeaders?: HeadersInit) {
|
||||
const baseHeaders = sourceHeaders ? new Headers(sourceHeaders) : new Headers(await headers())
|
||||
const sanitizedHeaders = new Headers(baseHeaders)
|
||||
|
||||
// Avoid ENOTFOUND by forcing host to localhost for internal better-auth fetches
|
||||
// We use the host defined in BETTER_AUTH_URL
|
||||
try {
|
||||
const betterAuthUrl = new URL(process.env.BETTER_AUTH_URL || 'http://localhost:3010')
|
||||
sanitizedHeaders.set('host', betterAuthUrl.host)
|
||||
} catch (e) {
|
||||
sanitizedHeaders.set('host', 'localhost:3010')
|
||||
}
|
||||
|
||||
return sanitizedHeaders
|
||||
}
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
||||
import { magicLink } from 'better-auth/plugins'
|
||||
import { admin as adminPlugin } from 'better-auth/plugins'
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
import { sendMagicLinkEmail } from './email'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: 'postgresql',
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
trustedOrigins: [
|
||||
process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3010',
|
||||
process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3010',
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001',
|
||||
'http://localhost:3010',
|
||||
'http://localhost:8081',
|
||||
'http://*.localhost:3010',
|
||||
'http://*.localhost:3000',
|
||||
'https://*.innungsapp.de',
|
||||
'https://*.innungsapp.com',
|
||||
// Additional origins from env (comma-separated)
|
||||
...(process.env.BETTER_AUTH_TRUSTED_ORIGINS ?? '').split(',').map((o) => o.trim()).filter(Boolean),
|
||||
],
|
||||
user: {
|
||||
additionalFields: {
|
||||
mustChangePassword: {
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, url }) => {
|
||||
await sendMagicLinkEmail({ to: email, magicUrl: url })
|
||||
},
|
||||
}),
|
||||
adminPlugin(),
|
||||
],
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type Auth = typeof auth
|
||||
|
||||
export async function getSanitizedHeaders(sourceHeaders?: HeadersInit) {
|
||||
const baseHeaders = sourceHeaders ? new Headers(sourceHeaders) : new Headers(await headers())
|
||||
const sanitizedHeaders = new Headers(baseHeaders)
|
||||
|
||||
// Avoid ENOTFOUND by forcing host to localhost for internal better-auth fetches
|
||||
// We use the host defined in BETTER_AUTH_URL
|
||||
try {
|
||||
const betterAuthUrl = new URL(process.env.BETTER_AUTH_URL || 'http://localhost:3010')
|
||||
sanitizedHeaders.set('host', betterAuthUrl.host)
|
||||
} catch (e) {
|
||||
sanitizedHeaders.set('host', 'localhost:3010')
|
||||
}
|
||||
|
||||
return sanitizedHeaders
|
||||
}
|
||||
|
||||
@@ -1,151 +1,151 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
const SMTP_HOST = (process.env.SMTP_HOST ?? '').trim()
|
||||
const SMTP_HOST_IS_PLACEHOLDER = SMTP_HOST === '' || SMTP_HOST.toLowerCase() === 'smtp.example.com'
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth:
|
||||
process.env.SMTP_USER
|
||||
? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
|
||||
: undefined,
|
||||
})
|
||||
|
||||
async function sendMailOrSkip(mailOptions: any, emailType: string) {
|
||||
if (SMTP_HOST_IS_PLACEHOLDER) {
|
||||
const target = typeof mailOptions?.to === 'string' ? mailOptions.to : 'unknown-recipient'
|
||||
console.warn(`[email] SMTP not configured. Skipping ${emailType} email to ${target}.`)
|
||||
return
|
||||
}
|
||||
|
||||
await transporter.sendMail(mailOptions)
|
||||
}
|
||||
|
||||
export async function sendMagicLinkEmail({
|
||||
to,
|
||||
magicUrl,
|
||||
}: {
|
||||
to: string
|
||||
magicUrl: string
|
||||
}) {
|
||||
await sendMailOrSkip({
|
||||
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
|
||||
to,
|
||||
subject: 'Ihr Login-Link für InnungsApp',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #E63946; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp</h1>
|
||||
</div>
|
||||
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<h2 style="color: #111827; margin-top: 0;">Ihr persönlicher Login-Link</h2>
|
||||
<p style="color: #4b5563;">Klicken Sie auf den folgenden Button, um sich einzuloggen. Der Link ist 24 Stunden gültig.</p>
|
||||
<a href="${magicUrl}"
|
||||
style="display: inline-block; background: #E63946; color: white; padding: 12px 24px;
|
||||
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
|
||||
Jetzt einloggen
|
||||
</a>
|
||||
<p style="color: #9ca3af; font-size: 14px;">
|
||||
Wenn Sie diesen Link nicht angefordert haben, können Sie diese E-Mail ignorieren.
|
||||
</p>
|
||||
<hr style="border-color: #e5e7eb; margin: 24px 0;" />
|
||||
<p style="color: #9ca3af; font-size: 12px; margin: 0;">
|
||||
InnungsApp · Die digitale Plattform für Innungen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}, 'magic link')
|
||||
}
|
||||
|
||||
export async function sendInviteEmail({
|
||||
to,
|
||||
memberName,
|
||||
orgName,
|
||||
apiUrl,
|
||||
}: {
|
||||
to: string
|
||||
memberName: string
|
||||
orgName: string
|
||||
apiUrl: string
|
||||
}) {
|
||||
// Generate magic link for the invite
|
||||
const signInUrl = `${apiUrl}/login?email=${encodeURIComponent(to)}&invited=true`
|
||||
|
||||
await sendMailOrSkip({
|
||||
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
|
||||
to,
|
||||
subject: `Einladung zur InnungsApp — ${orgName}`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #E63946; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp</h1>
|
||||
</div>
|
||||
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<h2 style="color: #111827; margin-top: 0;">Hallo ${memberName},</h2>
|
||||
<p style="color: #4b5563;">
|
||||
Sie wurden von der <strong>${orgName}</strong> zur InnungsApp eingeladen.
|
||||
InnungsApp ist die digitale Plattform Ihrer Innung für News, Termine und das Mitgliederverzeichnis.
|
||||
</p>
|
||||
<p style="color: #4b5563;">Klicken Sie auf den Button, um Ihren Account zu aktivieren:</p>
|
||||
<a href="${signInUrl}"
|
||||
style="display: inline-block; background: #E63946; color: white; padding: 12px 24px;
|
||||
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
|
||||
Jetzt Zugang aktivieren
|
||||
</a>
|
||||
<p style="color: #9ca3af; font-size: 14px;">Kein Passwort nötig — Sie erhalten einen sicheren Login-Link per E-Mail.</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}, 'invite')
|
||||
}
|
||||
|
||||
export async function sendAdminCredentialsEmail({
|
||||
to,
|
||||
adminName,
|
||||
orgName,
|
||||
password,
|
||||
loginUrl,
|
||||
}: {
|
||||
to: string
|
||||
adminName: string
|
||||
orgName: string
|
||||
password: string
|
||||
loginUrl: string
|
||||
}) {
|
||||
await sendMailOrSkip({
|
||||
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
|
||||
to,
|
||||
subject: `Admin-Zugang für — ${orgName}`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #111827; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp Admin</h1>
|
||||
</div>
|
||||
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<h2 style="color: #111827; margin-top: 0;">Hallo ${adminName},</h2>
|
||||
<p style="color: #4b5563;">
|
||||
Sie wurden als Administrator für die <strong>${orgName}</strong> in der InnungsApp freigeschaltet.
|
||||
</p>
|
||||
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; margin: 24px 0;">
|
||||
<p style="margin-top: 0; font-size: 14px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;">Ihre Zugangsdaten</p>
|
||||
<p style="margin: 8px 0; font-size: 16px; color: #111827;"><strong>E-Mail:</strong> ${to}</p>
|
||||
<p style="margin: 8px 0; font-size: 16px; color: #111827;"><strong>Passwort:</strong> <code style="background: #eee; padding: 2px 4px; rounded: 4px;">${password}</code></p>
|
||||
</div>
|
||||
<p style="color: #4b5563;">Klicken Sie auf den Button, um sich im Verwaltungsportal anzumelden. Sie werden aufgefordert, Ihr Passwort nach dem ersten Login zu ändern.</p>
|
||||
<a href="${loginUrl}/login?email=${encodeURIComponent(to)}"
|
||||
style="display: inline-block; background: #111827; color: white; padding: 12px 24px;
|
||||
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
|
||||
Zum Admin-Portal
|
||||
</a>
|
||||
<hr style="border-color: #e5e7eb; margin: 24px 0;" />
|
||||
<p style="color: #9ca3af; font-size: 12px; margin: 0;">
|
||||
InnungsApp · Administrative Portal
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}, 'admin credentials')
|
||||
}
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
const SMTP_HOST = (process.env.SMTP_HOST ?? '').trim()
|
||||
const SMTP_HOST_IS_PLACEHOLDER = SMTP_HOST === '' || SMTP_HOST.toLowerCase() === 'smtp.example.com'
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth:
|
||||
process.env.SMTP_USER
|
||||
? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
|
||||
: undefined,
|
||||
})
|
||||
|
||||
async function sendMailOrSkip(mailOptions: any, emailType: string) {
|
||||
if (SMTP_HOST_IS_PLACEHOLDER) {
|
||||
const target = typeof mailOptions?.to === 'string' ? mailOptions.to : 'unknown-recipient'
|
||||
console.warn(`[email] SMTP not configured. Skipping ${emailType} email to ${target}.`)
|
||||
return
|
||||
}
|
||||
|
||||
await transporter.sendMail(mailOptions)
|
||||
}
|
||||
|
||||
export async function sendMagicLinkEmail({
|
||||
to,
|
||||
magicUrl,
|
||||
}: {
|
||||
to: string
|
||||
magicUrl: string
|
||||
}) {
|
||||
await sendMailOrSkip({
|
||||
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
|
||||
to,
|
||||
subject: 'Ihr Login-Link für InnungsApp',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #E63946; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp</h1>
|
||||
</div>
|
||||
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<h2 style="color: #111827; margin-top: 0;">Ihr persönlicher Login-Link</h2>
|
||||
<p style="color: #4b5563;">Klicken Sie auf den folgenden Button, um sich einzuloggen. Der Link ist 24 Stunden gültig.</p>
|
||||
<a href="${magicUrl}"
|
||||
style="display: inline-block; background: #E63946; color: white; padding: 12px 24px;
|
||||
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
|
||||
Jetzt einloggen
|
||||
</a>
|
||||
<p style="color: #9ca3af; font-size: 14px;">
|
||||
Wenn Sie diesen Link nicht angefordert haben, können Sie diese E-Mail ignorieren.
|
||||
</p>
|
||||
<hr style="border-color: #e5e7eb; margin: 24px 0;" />
|
||||
<p style="color: #9ca3af; font-size: 12px; margin: 0;">
|
||||
InnungsApp · Die digitale Plattform für Innungen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}, 'magic link')
|
||||
}
|
||||
|
||||
export async function sendInviteEmail({
|
||||
to,
|
||||
memberName,
|
||||
orgName,
|
||||
apiUrl,
|
||||
}: {
|
||||
to: string
|
||||
memberName: string
|
||||
orgName: string
|
||||
apiUrl: string
|
||||
}) {
|
||||
// Generate magic link for the invite
|
||||
const signInUrl = `${apiUrl}/login?email=${encodeURIComponent(to)}&invited=true`
|
||||
|
||||
await sendMailOrSkip({
|
||||
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
|
||||
to,
|
||||
subject: `Einladung zur InnungsApp — ${orgName}`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #E63946; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp</h1>
|
||||
</div>
|
||||
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<h2 style="color: #111827; margin-top: 0;">Hallo ${memberName},</h2>
|
||||
<p style="color: #4b5563;">
|
||||
Sie wurden von der <strong>${orgName}</strong> zur InnungsApp eingeladen.
|
||||
InnungsApp ist die digitale Plattform Ihrer Innung für News, Termine und das Mitgliederverzeichnis.
|
||||
</p>
|
||||
<p style="color: #4b5563;">Klicken Sie auf den Button, um Ihren Account zu aktivieren:</p>
|
||||
<a href="${signInUrl}"
|
||||
style="display: inline-block; background: #E63946; color: white; padding: 12px 24px;
|
||||
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
|
||||
Jetzt Zugang aktivieren
|
||||
</a>
|
||||
<p style="color: #9ca3af; font-size: 14px;">Kein Passwort nötig — Sie erhalten einen sicheren Login-Link per E-Mail.</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}, 'invite')
|
||||
}
|
||||
|
||||
export async function sendAdminCredentialsEmail({
|
||||
to,
|
||||
adminName,
|
||||
orgName,
|
||||
password,
|
||||
loginUrl,
|
||||
}: {
|
||||
to: string
|
||||
adminName: string
|
||||
orgName: string
|
||||
password: string
|
||||
loginUrl: string
|
||||
}) {
|
||||
await sendMailOrSkip({
|
||||
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
|
||||
to,
|
||||
subject: `Admin-Zugang für — ${orgName}`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #111827; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp Admin</h1>
|
||||
</div>
|
||||
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<h2 style="color: #111827; margin-top: 0;">Hallo ${adminName},</h2>
|
||||
<p style="color: #4b5563;">
|
||||
Sie wurden als Administrator für die <strong>${orgName}</strong> in der InnungsApp freigeschaltet.
|
||||
</p>
|
||||
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; margin: 24px 0;">
|
||||
<p style="margin-top: 0; font-size: 14px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;">Ihre Zugangsdaten</p>
|
||||
<p style="margin: 8px 0; font-size: 16px; color: #111827;"><strong>E-Mail:</strong> ${to}</p>
|
||||
<p style="margin: 8px 0; font-size: 16px; color: #111827;"><strong>Passwort:</strong> <code style="background: #eee; padding: 2px 4px; rounded: 4px;">${password}</code></p>
|
||||
</div>
|
||||
<p style="color: #4b5563;">Klicken Sie auf den Button, um sich im Verwaltungsportal anzumelden. Sie werden aufgefordert, Ihr Passwort nach dem ersten Login zu ändern.</p>
|
||||
<a href="${loginUrl}/login?email=${encodeURIComponent(to)}"
|
||||
style="display: inline-block; background: #111827; color: white; padding: 12px 24px;
|
||||
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
|
||||
Zum Admin-Portal
|
||||
</a>
|
||||
<hr style="border-color: #e5e7eb; margin: 24px 0;" />
|
||||
<p style="color: #9ca3af; font-size: 12px; margin: 0;">
|
||||
InnungsApp · Administrative Portal
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}, 'admin credentials')
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
const RESERVED_SUBDOMAINS = ['www', 'app', 'admin', 'localhost', 'superadmin', 'api']
|
||||
|
||||
export async function getTenantSlug() {
|
||||
const host = (await headers()).get('host') || ''
|
||||
const domainParts = host.split(':')[0].split('.')
|
||||
|
||||
if (
|
||||
domainParts.length > 2 ||
|
||||
(domainParts.length === 2 && domainParts[1] === 'localhost')
|
||||
) {
|
||||
const slug = domainParts[0]
|
||||
if (!RESERVED_SUBDOMAINS.includes(slug)) {
|
||||
return slug
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
const RESERVED_SUBDOMAINS = ['www', 'app', 'admin', 'localhost', 'superadmin', 'api']
|
||||
|
||||
export async function getTenantSlug() {
|
||||
const host = (await headers()).get('host') || ''
|
||||
const domainParts = host.split(':')[0].split('.')
|
||||
|
||||
if (
|
||||
domainParts.length > 2 ||
|
||||
(domainParts.length === 2 && domainParts[1] === 'localhost')
|
||||
) {
|
||||
const slug = domainParts[0]
|
||||
if (!RESERVED_SUBDOMAINS.includes(slug)) {
|
||||
return slug
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user