8000 ENG-1365 - Adamk/roblox connection for students by adamkecskes · Pull Request #7831 · codecombat/codecombat · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

ENG-1365 - Adamk/roblox connection for students #7831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/core/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
levels: require('./levels'),
levelSessions: require('./level-sessions'),
markdown: require('./markdown'),
oauth2identity: require('./oauth2identity'),
prepaids: require('./prepaids'),
resourceHubResources: require('./resource_hub_resource'),
skippedContacts: require('./skipped-contacts'),
Expand Down
16 changes: 16 additions & 0 deletions app/core/api/oauth2identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const fetchJson = require('./fetch-json')

module.exports = {

Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing lodash import

The code uses _.assign but lodash is not imported. This will cause a runtime error.

Add the following import at the top of the file:

const fetchJson = require('./fetch-json')
+const _ = require('lodash')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchJson = require('./fetch-json')
module.exports = {
const fetchJson = require('./fetch-json')
const _ 10000 = require('lodash')
module.exports = {

post (options) {
if (options == null) { options = {} }
return fetchJson('/db/oauth2identity', _.assign({}, {
method: 'POST',
json: options,
}))
},
Comment on lines +5 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and parameter validation

The current implementation could benefit from several improvements:

  1. Use ES6 default parameters instead of null check
  2. Add type validation for options
  3. Add error handling for the fetch operation

Consider this implementation:

-  post (options) {
-    if (options == null) { options = {} }
-    return fetchJson('/db/oauth2identity', _.assign({}, {
-      method: 'POST',
-      json: options,
-    }))
+  post (options = {}) {
+    if (typeof options !== 'object') {
+      throw new TypeError('Options must be an object')
+    }
+    return fetchJson('/db/oauth2identity', {
+      method: 'POST',
+      json: options,
+    }).catch(error => {
+      console.error('Failed to post OAuth2 identity:', error)
+      throw error
+    })
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
post (options) {
if (options == null) { options = {} }
return fetchJson('/db/oauth2identity', _.assign({}, {
method: 'POST',
json: options,
}))
},
post (options = {}) {
if (typeof options !== 'object') {
throw new TypeError('Options must be an object')
}
return fetchJson('/db/oauth2identity', {
method: 'POST',
json: options,
}).catch(error => {
console.error('Failed to post OAuth2 identity:', error)
throw error
})
}


fetchForProviderAndUser (provider, userId) {
return fetchJson('/db/oauth2identity/by-user', { data: { userId, provider } })
},
}
2 changes: 2 additions & 0 deletions app/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2013,7 +2013,9 @@ module.exports = {
re_connect_roblox_button: 'Connect Another Account',
disconnect_roblox_button: 'Disconnect',
roblox_connected: 'Your account <strong>__username__</strong> is connected to Roblox.',
roblox_connected_other_user: 'The student\'s account is connected to <strong>__username__</strong> Roblox account.',
roblox_not_connected: 'Connect your CodeCombat and Roblox accounts.',
roblox_not_connected_other_user: 'Connect the student\'s CodeCombat and Roblox accounts.',
roblox_disconnect_confirm: 'Are you sure you want to disconnect your Roblox account?',
god_mode: 'God Mode',
emails_tab: 'Emails',
Expand Down
45 changes: 25 additions & 20 deletions app/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const UserLib = {
}
if (name) { return name }
({
name
name,
} = user)
if (name) { return name }
const [emailName, emailDomain] = Array.from(user.email?.split('@') || []) // eslint-disable-line no-unused-vars
Expand All @@ -53,7 +53,7 @@ const UserLib = {
isTeacher (user, includePossibleTeachers = false) {
if (includePossibleTeachers && (user.role === 'possible teacher')) { return true } // They maybe haven't created an account but we think they might be a teacher based on behavior
return ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent'].includes(user.role)
}
},
}

module.exports = (User = (function () {
Expand Down Expand Up @@ -81,7 +81,7 @@ module.exports = (User = (function () {
ONLINE_TEACHER: 'onlineTeacher',
BETA_TESTER: 'betaTester',
PARENT_ADMIN: 'parentAdmin',
NAPERVILLE_ADMIN: 'napervilleAdmin'
NAPERVILLE_ADMIN: 'napervilleAdmin',
}

a = 5
Expand Down Expand Up @@ -173,7 +173,7 @@ module.exports = (User = (function () {
isNewDashboardActive () {
const features = {
isNewDashboardActive: true,
...(this.get('features') || {})
...(this.get('features') || {}),
}
return features.isNewDashboardActive
}
Expand Down Expand Up @@ -221,26 +221,26 @@ module.exports = (User = (function () {
// deprecate in favor of @checkNameConflicts, which uses Promises and returns the whole response
return $.ajax(`/auth/name/${encodeURIComponent(name)}`, {
cache: false,
success (data) { return done(data.suggestedName) }
}
success (data) { return done(data.suggestedName) },
},
)
}

static checkNameConflicts (name) {
return new Promise((resolve, reject) => $.ajax(`/auth/name/${encodeURIComponent(name)}`, {
cache: false,
success: resolve,
error (jqxhr) { return reject(jqxhr.responseJSON) }
}
error (jqxhr) { return reject(jqxhr.responseJSON) },
},
))
}

static checkEmailExists (email) {
return new Promise((resolve, reject) => $.ajax(`/auth/email/${encodeURIComponent(email)}`, {
cache: false,
success: resolve,
error (jqxhr) { return reject(jqxhr.responseJSON) }
}
error (jqxhr) { return reject(jqxhr.responseJSON) },
},
))
}

Expand Down Expand Up @@ -281,6 +281,11 @@ module.exports = (User = (function () {

isStudent () { return this.get('role') === 'student' }

canUseRobloxOauthConnection () {
// No Roblox OAuth in classroom mode
return !this.isTeacher() && !this.isStudent()
}

isTestStudent () { return this.isStudent() && (this.get('related') || []).some(({ relation }) => relation === 'TestStudent') }

isCreatedByClient () { return (this.get('clientCreator') != null) }
Expand Down Expand Up @@ -528,7 +533,7 @@ module.exports = (User = (function () {
if (products) {
const homeProducts = this.activeProducts('basic_subscription')
const {
endDate
endDate,
} = _.max(homeProducts, p => new Date(p.endDate))
const productsEnd = moment(endDate)
if (stripeEnd && stripeEnd.isAfter(productsEnd)) { return stripeEnd.utc().format('ll') }
Expand Down Expand Up @@ -557,7 +562,7 @@ module.exports = (User = (function () {
},
error: () => {
return this.trigger('email-verify-error')
}
},
})
}

Expand All @@ -571,7 +576,7 @@ module.exports = (User = (function () {
},
error: () => {
return this.trigger('user-keep-me-updated-error')
}
},
})
}

Expand All @@ -584,7 +589,7 @@ module.exports = (User = (function () {
this.set(attributes)
return success()
},
error
error,
})
}

Expand All @@ -598,7 +603,7 @@ module.exports = (User = (function () {
},
error: () => {
return this.trigger('user-no-delete-eu-error')
}
},
})
}

Expand All @@ -611,7 +616,7 @@ module.exports = (User = (function () {
},
error () {
return console.error(`Couldn't save activity ${activityName}`)
}
},
})
}

Expand All @@ -631,7 +636,7 @@ module.exports = (User = (function () {
},
error (jqxhr) {
return console.error(`Couldn't start experiment ${name}:`, jqxhr.responseJSON)
}
},
})
const experiment = { name, value, startDate: new Date() } // Server date/save will be authoritative
if (probability != null) { experiment.probability = probability }
Expand Down Expand Up @@ -963,7 +968,7 @@ module.exports = (User = (function () {
type: 'course',
includedCourseIDs: courseProduct?.productOptions?.includedCourseIDs,
startDate: courseProduct.startDate,
endDate: courseProduct.endDate
endDate: courseProduct.endDate,
})
}

Expand Down Expand Up @@ -1066,7 +1071,7 @@ module.exports = (User = (function () {

getFilteredExperimentValue ({
experimentName,
forcedValue
forcedValue,
}) {
let value = me.getExperimentValue(experimentName, null)

Expand Down Expand Up @@ -1543,7 +1548,7 @@ module.exports = (User = (function () {
})())

const tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15,
]

// Make UserLib accessible via eg. User.broadName(userObj)
Expand Down
88 changes: 88 additions & 0 deletions app/views/account/RobloxIdentityField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script>
import VueConfirmDialog from 'vue-confirm-dialog'
import oauth2identity from 'core/api/oauth2identity'

Vue.use(VueConfirmDialog)
Vue.component('VueConfirmDialog', VueConfirmDialog.default)

export default Vue.extend({
props: {
userId: {
type: String,
required: true,
},
},
data () {
return {
isAnonymous: me.isAnonymous(),
robloxID: '',
}
},
Comment on lines +15 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing import and Roblox ID validation.

The component uses me.isAnonymous() without importing it. Additionally, consider adding validation for the Roblox ID format.

  1. Add the missing import:
 import Vue from 'vue'
 import VueConfirmDialog from 'vue-confirm-dialog'
 import oauth2identity from 'core/api/oauth2identity'
+import { me } from 'core/utils'
  1. Add validation computed property:
+  computed: {
+    isValidRobloxID() {
+      return /^\d+$/.test(this.robloxID.trim())
+    }
+  },

Committable suggestion skipped: line range outside the PR's diff.


methods: {
async createNewIdentity () {
await oauth2identity.post({
ownerID: this.userId,
provider: 'roblox',
profile: {
sub: this.robloxID,
},
})
},
async onSave () {
if (this.robloxID) {
this.$refs.connectRobloxButton.disabled = true
try {
await this.createNewIdentity()
this.$emit('saved')
} catch (error) {
noty({
text: error.message,
type: 'error',
timeout: 5000,
})
this.$refs.robloxID.select()
} finally {
if (this.$refs.connectRobloxButton) {
this.$refs.connectRobloxButton.disabled = false
}
}
}
},
},
Comment on lines +22 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance security and robustness of the save operation.

Several improvements can be made to the methods implementation:

  1. Sanitize input before API call
  2. Handle specific error types
  3. Improve button state management
  4. Add loading state

Apply these changes:

 methods: {
   async createNewIdentity () {
+    const sanitizedID = this.robloxID.trim()
+    if (!this.isValidRobloxID) {
+      throw new Error(this.$t('account_settings.invalid_roblox_id'))
+    }
     await oauth2identity.post({
       ownerID: this.userId,
       provider: 'roblox',
       profile: {
-        sub: this.robloxID,
+        sub: sanitizedID,
       },
     })
   },
   async onSave () {
-    if (this.robloxID) {
-      this.$refs.connectRobloxButton.disabled = true
+    if (this.robloxID.trim()) {
+      this.isLoading = true
       try {
         await this.createNewIdentity()
         this.$emit('saved')
       } catch (error) {
+        const errorMessage = error.response?.status === 409
+          ? this.$t('account_settings.roblox_id_already_connected')
+          : this.$t('account_settings.connection_error')
         noty({
-          text: error.message,
+          text: errorMessage,
           type: 'error',
           timeout: 5000,
         })
         this.$refs.robloxID.select()
       } finally {
-        if (this.$refs.connectRobloxButton) {
-          this.$refs.connectRobloxButton.disabled = false
-        }
+        this.isLoading = false
       }
     }
   },
 },

Don't forget to add the loading state to the data:

 data () {
   return {
     isAnonymous: me.isAnonymous(),
     robloxID: '',
+    isLoading: false,
   }
 },

Committable suggestion skipped: line range outside the PR's diff.

})
</script>

<template>
<div class="roblox-id form-inline">
<div class="form-group">
<input
ref="robloxID"
v-model="robloxID"
type="text"
class="roblox-id__input form-control"
placeholder="Roblox ID"
>
<button
ref="connectRobloxButton"
class="btn form-control btn-primary"
@click="onSave"
>
{{ $t('account_settings.connect_roblox_button') }}
</button>
</div>
</div>
</template>
Comment on lines +56 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance template with validation feedback and accessibility.

The template needs improvements for better user experience and accessibility.

 <template>
   <div class="roblox-id form-inline">
     <div class="form-group">
       <input
         ref="robloxID"
         v-model="robloxID"
         type="text"
         class="roblox-id__input form-control"
+        :class="{ 'is-invalid': robloxID && !isValidRobloxID }"
         placeholder="Roblox ID"
+        aria-label="Roblox ID"
+        :aria-invalid="!isValidRobloxID"
       >
+      <div v-if="robloxID && !isValidRobloxID" class="invalid-feedback">
+        {{ $t('account_settings.invalid_roblox_id') }}
+      </div>
       <button
         ref="connectRobloxButton"
-        class="btn form-control btn-primary"
+        class="btn form-control btn-primary" 
+        :disabled="isLoading || !isValidRobloxID"
         @click="onSave"
+        aria-label="Connect Roblox Account"
       >
-        {{ $t('account_settings.connect_roblox_button') }}
+        <span v-if="isLoading">
+          <i class="fa fa-spinner fa-spin"></i>
+        </span>
+        <span v-else>
+          {{ $t('account_settings.connect_roblox_button') }}
+        </span>
       </button>
     </div>
   </div>
 </template>

Committable suggestion skipped: line range outside the PR's diff.


<style lang="scss" scoped>
.roblox-id {
width: 100%;
.form-group {
display: flex;
gap: 15px;
.roblox-id__input {
width: 100%;
}
}
}
</style>
Loading
Loading
0