Skip to content

Commit

Permalink
Merge branch 'master' into feature/STUD-196
Browse files Browse the repository at this point in the history
  • Loading branch information
saif-software-developer committed Jul 27, 2024
2 parents 0be2c82 + 3509367 commit 654584a
Show file tree
Hide file tree
Showing 13 changed files with 349 additions and 317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import io.newm.server.auth.oauth.model.OAuthLoginRequest
import io.newm.server.auth.oauth.model.OAuthType
import io.newm.server.auth.oauth.repo.OAuthRepository
import io.newm.server.auth.password.createLoginResponse
import io.newm.shared.koin.inject
import io.newm.shared.exception.HttpBadRequestException
import io.newm.server.features.user.repo.UserRepository
import io.newm.server.ktx.clientPlatform
import io.newm.server.recaptcha.repo.RecaptchaRepository
import io.newm.shared.exception.HttpBadRequestException
import io.newm.shared.koin.inject
import io.newm.shared.ktx.post

fun Routing.createOAuthRoutes(type: OAuthType) {
Expand All @@ -25,11 +26,10 @@ fun Routing.createOAuthRoutes(type: OAuthType) {
post("$AUTH_PATH/login/$typeName") {
recaptchaRepository.verify("login_$typeName", request)
val req = receive<OAuthLoginRequest>()
val oauthTokens =
req.oauthTokens ?: req.code?.let { code ->
oAuthRepository.getTokens(type, code, req.redirectUri)
} ?: throw HttpBadRequestException("missing code")
val userId = userRepository.findOrAdd(type, oauthTokens)
val oauthTokens = req.oauthTokens ?: req.code?.let { code ->
oAuthRepository.getTokens(type, code, req.redirectUri)
} ?: throw HttpBadRequestException("missing code")
val userId = userRepository.findOrAdd(type, oauthTokens, clientPlatform)
val isAdmin = userRepository.isAdmin(userId)
respond(jwtRepository.createLoginResponse(userId, isAdmin))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.newm.server.database.migration

import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.jetbrains.exposed.sql.transactions.transaction

@Suppress("unused")
class V63__UsersUpdates : BaseJavaMigration() {
override fun migrate(context: Context?) {
transaction {
exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS signup_platform integer NOT NULL DEFAULT 0")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.newm.server.features.model.CountResponse
import io.newm.server.features.user.model.UserIdBody
import io.newm.server.features.user.model.userFilters
import io.newm.server.features.user.repo.UserRepository
import io.newm.server.ktx.clientPlatform
import io.newm.server.ktx.identifyUser
import io.newm.server.ktx.limit
import io.newm.server.ktx.offset
Expand All @@ -29,40 +30,38 @@ fun Routing.createUserRoutes() {
val recaptchaRepository: RecaptchaRepository by inject()
val userRepository: UserRepository by inject()

authenticate(AUTH_JWT) {
route("$USERS_PATH/{userId}") {
get {
identifyUser { userId, isMe ->
respond(userRepository.get(userId, isMe))
route(USERS_PATH) {
authenticate(AUTH_JWT) {
route("{userId}") {
get {
identifyUser { userId, isMe ->
respond(userRepository.get(userId, isMe))
}
}
}
patch {
restrictToMe { myUserId ->
userRepository.update(myUserId, receive())
respond(HttpStatusCode.NoContent)
patch {
restrictToMe { myUserId ->
userRepository.update(myUserId, receive())
respond(HttpStatusCode.NoContent)
}
}
}
delete {
restrictToMe { myUserId ->
userRepository.delete(myUserId)
respond(HttpStatusCode.NoContent)
delete {
restrictToMe { myUserId ->
userRepository.delete(myUserId)
respond(HttpStatusCode.NoContent)
}
}
}
}
route(USERS_PATH) {

get {
respond(userRepository.getAll(userFilters, offset, limit))
}
get("count") {
respond(CountResponse(userRepository.getAllCount(userFilters)))
}
}
}

route(USERS_PATH) {
post {
recaptchaRepository.verify("signup", request)
respond(UserIdBody(userRepository.add(receive())))
respond(UserIdBody(userRepository.add(receive(), clientPlatform)))
}
put("password") {
recaptchaRepository.verify("password_reset", request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.newm.server.auth.oauth.model.OAuthType
import io.newm.server.features.user.model.User
import io.newm.server.features.user.model.UserFilters
import io.newm.server.features.user.model.UserVerificationStatus
import io.newm.server.model.ClientPlatform
import io.newm.server.typealiases.UserId
import io.newm.shared.ktx.exists
import org.jetbrains.exposed.dao.UUIDEntity
Expand All @@ -24,6 +25,7 @@ open class UserEntity(
id: EntityID<UserId>
) : UUIDEntity(id) {
val createdAt: LocalDateTime by UserTable.createdAt
var signupPlatform: ClientPlatform by UserTable.signupPlatform
var oauthType: OAuthType? by UserTable.oauthType
var oauthId: String? by UserTable.oauthId
var firstName: String? by UserTable.firstName
Expand Down Expand Up @@ -65,6 +67,7 @@ open class UserEntity(
User(
id = id.value,
createdAt = createdAt,
signupPlatform = signupPlatform.takeIf { includeAll },
oauthType = oauthType.takeIf { includeAll },
oauthId = oauthId.takeIf { includeAll },
firstName = firstName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.newm.server.features.user.database

import io.newm.server.auth.oauth.model.OAuthType
import io.newm.server.features.user.model.UserVerificationStatus
import io.newm.server.model.ClientPlatform
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.javatime.CurrentDateTime
Expand All @@ -11,6 +12,8 @@ import java.time.LocalDateTime
object UserTable : UUIDTable(name = "users") {
val createdAt: Column<LocalDateTime> = datetime("created_at").defaultExpression(CurrentDateTime)
val oauthType: Column<OAuthType?> = enumeration("oauth_type", OAuthType::class).nullable()
val signupPlatform: Column<ClientPlatform> =
enumeration("signup_platform", ClientPlatform::class).default(ClientPlatform.Studio)
val oauthId: Column<String?> = text("oauth_id").nullable()
val firstName: Column<String?> = text("first_name").nullable()
val lastName: Column<String?> = text("last_name").nullable()
Expand All @@ -31,8 +34,7 @@ object UserTable : UUIDTable(name = "users") {
val email: Column<String> = text("email")
val passwordHash: Column<String?> = text("password_hash").nullable()
val verificationStatus: Column<UserVerificationStatus> =
enumeration("verification_status", UserVerificationStatus::class)
.default(UserVerificationStatus.Unverified)
enumeration("verification_status", UserVerificationStatus::class).default(UserVerificationStatus.Unverified)
val companyName: Column<String?> = text("company_name").nullable()
val companyLogoUrl: Column<String?> = text("company_logo_url").nullable()
val companyIpRights: Column<Boolean?> = bool("company_ip_rights").nullable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.newm.server.features.user.model

import com.google.common.annotations.VisibleForTesting
import io.newm.server.auth.oauth.model.OAuthType
import io.newm.server.model.ClientPlatform
import io.newm.server.typealiases.UserId
import io.newm.shared.auth.Password
import io.newm.shared.serialization.LocalDateTimeSerializer
Expand All @@ -17,6 +18,7 @@ data class User(
@Serializable(with = LocalDateTimeSerializer::class)
val createdAt: LocalDateTime? = null,
val oauthType: OAuthType? = null,
val signupPlatform: ClientPlatform? = null,
val oauthId: String? = null,
val firstName: String? = null,
val lastName: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import io.newm.server.auth.oauth.model.OAuthTokens
import io.newm.server.auth.oauth.model.OAuthType
import io.newm.server.features.user.model.User
import io.newm.server.features.user.model.UserFilters
import io.newm.server.model.ClientPlatform
import io.newm.server.typealiases.UserId
import io.newm.shared.auth.Password

interface UserRepository {
suspend fun add(user: User): UserId
suspend fun add(
user: User,
clientPlatform: ClientPlatform?
): UserId

suspend fun find(
email: String,
Expand All @@ -19,7 +23,8 @@ interface UserRepository {

suspend fun findOrAdd(
oauthType: OAuthType,
oauthTokens: OAuthTokens
oauthTokens: OAuthTokens,
clientPlatform: ClientPlatform?
): UserId

suspend fun exists(userId: UserId): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.newm.server.ktx.asValidEmail
import io.newm.server.ktx.asValidName
import io.newm.server.ktx.asValidUrl
import io.newm.server.ktx.checkLength
import io.newm.server.model.ClientPlatform
import io.newm.server.typealiases.UserId
import io.newm.shared.auth.Password
import io.newm.shared.exception.HttpBadRequestException
Expand Down Expand Up @@ -52,8 +53,11 @@ internal class UserRepositoryImpl(
) : UserRepository {
private val logger = KotlinLogging.logger {}

override suspend fun add(user: User): UserId {
logger.debug { "add: user = $user" }
override suspend fun add(
user: User,
clientPlatform: ClientPlatform?
): UserId {
logger.debug { "add: user = $user, clientPlatform = $clientPlatform" }

user.checkWhitelist()
user.checkFieldLengths()
Expand All @@ -79,6 +83,7 @@ internal class UserRepositoryImpl(
email.checkEmailUnique()
UserEntity
.new {
this.signupPlatform = clientPlatform ?: ClientPlatform.Studio
this.firstName = user.firstName?.asValidName()
this.lastName = user.lastName?.asValidName()
this.nickname = user.nickname
Expand Down Expand Up @@ -130,17 +135,17 @@ internal class UserRepositoryImpl(

override suspend fun findOrAdd(
oauthType: OAuthType,
oauthTokens: OAuthTokens
oauthTokens: OAuthTokens,
clientPlatform: ClientPlatform?
): UserId {
logger.debug { "findOrAdd: oauthType = $oauthType" }

val user =
when (oauthType) {
OAuthType.Google -> googleUserProvider.getUser(oauthTokens)
OAuthType.Facebook -> facebookUserProvider.getUser(oauthTokens)
OAuthType.LinkedIn -> linkedInUserProvider.getUser(oauthTokens)
OAuthType.Apple -> appleUserProvider.getUser(oauthTokens)
}
val user = when (oauthType) {
OAuthType.Google -> googleUserProvider.getUser(oauthTokens)
OAuthType.Facebook -> facebookUserProvider.getUser(oauthTokens)
OAuthType.LinkedIn -> linkedInUserProvider.getUser(oauthTokens)
OAuthType.Apple -> appleUserProvider.getUser(oauthTokens)
}
logger.debug { "findOrAdd: oauthUser = $user" }

user.checkWhitelist()
Expand All @@ -149,13 +154,13 @@ internal class UserRepositoryImpl(
throw HttpUnauthorizedException("Unverified email: $email")
}
return transaction {
val entity =
UserEntity.getByEmail(email) ?: UserEntity.new {
this.firstName = user.firstName?.asValidName()
this.lastName = user.lastName?.asValidName()
this.pictureUrl = user.pictureUrl?.asValidUrl()
this.email = email
}
val entity = UserEntity.getByEmail(email) ?: UserEntity.new {
this.signupPlatform = clientPlatform ?: ClientPlatform.Studio
this.firstName = user.firstName?.asValidName()
this.lastName = user.lastName?.asValidName()
this.pictureUrl = user.pictureUrl?.asValidUrl()
this.email = email
}
entity.oauthType = oauthType
entity.oauthId = user.id
entity.id.value
Expand Down Expand Up @@ -231,22 +236,19 @@ internal class UserRepositoryImpl(
user.instagramUrl?.let { entity.instagramUrl = it.orNull()?.asValidUrl() }
try {
user.spotifyProfile?.let {
entity.spotifyProfile =
it
.asUrlWithHost("open.spotify.com")
?.also { profile -> spotifyProfileUrlVerifier.verify(profile, entity.stageOrFullName) }
entity.spotifyProfile = it
.asUrlWithHost("open.spotify.com")
?.also { profile -> spotifyProfileUrlVerifier.verify(profile, entity.stageOrFullName) }
}
user.soundCloudProfile?.let {
entity.soundCloudProfile =
it
.asUrlWithHost("soundcloud.com")
?.also { profile -> soundCloudProfileUrlVerifier.verify(profile, entity.stageOrFullName) }
entity.soundCloudProfile = it
.asUrlWithHost("soundcloud.com")
?.also { profile -> soundCloudProfileUrlVerifier.verify(profile, entity.stageOrFullName) }
}
user.appleMusicProfile?.let {
entity.appleMusicProfile =
it
.asUrlWithHost("music.apple.com")
?.also { profile -> appleMusicProfileUrlVerifier.verify(profile, entity.stageOrFullName) }
entity.appleMusicProfile = it
.asUrlWithHost("music.apple.com")
?.also { profile -> appleMusicProfileUrlVerifier.verify(profile, entity.stageOrFullName) }
}
} catch (exception: OutletProfileUrlVerificationException) {
val message = exception.message ?: exception.toString()
Expand Down Expand Up @@ -395,10 +397,9 @@ internal class UserRepositoryImpl(

private suspend fun checkWhitelist(email: String) {
if (configRepository.exists(CONFIG_KEY_EMAIL_WHITELIST)) {
val whitelistRegexList =
configRepository.getStrings(CONFIG_KEY_EMAIL_WHITELIST).map {
Regex(it, RegexOption.IGNORE_CASE)
}
val whitelistRegexList = configRepository.getStrings(CONFIG_KEY_EMAIL_WHITELIST).map {
Regex(it, RegexOption.IGNORE_CASE)
}
if (whitelistRegexList.none { it.matches(email) }) {
logger.error { "Email not whitelisted: $email" }
throw HttpUnauthorizedException("Email not whitelisted: $email")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.ktor.server.response.respond
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.bits.Memory
import io.newm.server.features.song.model.MintingStatus
import io.newm.server.model.ClientPlatform
import io.newm.server.model.FilterCriteria
import io.newm.server.model.toFilterCriteria
import io.newm.server.model.toStringFilterCriteria
Expand Down Expand Up @@ -115,6 +116,9 @@ val ApplicationCall.artistId: UserId
val ApplicationCall.artistIds: FilterCriteria<UserId>?
get() = parameters["artistIds"]?.toUUIDFilterCriteria()

val ApplicationCall.clientPlatform: ClientPlatform?
get() = parameters["clientPlatform"]?.let(ClientPlatform::valueOf)

suspend inline fun ApplicationCall.identifyUser(crossinline body: suspend ApplicationCall.(UUID, Boolean) -> Unit) {
val uid = userId
body(uid, uid == myUserId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.newm.server.model

enum class ClientPlatform {
Studio,
Android,
IOS
}
Loading

0 comments on commit 654584a

Please sign in to comment.