Browse Source

學生OpenID Login API

jeff 1 year ago
parent
commit
b267039e6f
1 changed files with 315 additions and 1 deletions
  1. 315 1
      TEAMModelOS/Controllers/Student/StudentController.cs

+ 315 - 1
TEAMModelOS/Controllers/Student/StudentController.cs

@@ -6,6 +6,7 @@ using System.IdentityModel.Tokens.Jwt;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Text;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -17,6 +18,7 @@ using HTEXLib.COMM.Helpers;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Cryptography.KeyDerivation;
 using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Hosting;
@@ -52,7 +54,8 @@ namespace TEAMModelOS.Controllers
         private readonly IWebHostEnvironment _environment;
         public IConfiguration _configuration { get; set; }
         private readonly AzureServiceBusFactory _serviceBus;
-        public StudentController(IWebHostEnvironment environment, CoreAPIHttpService coreAPIHttpService, AzureCosmosFactory azureCosmos, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, DingDing dingDing, IPSearcher searcher, IOptionsSnapshot<Option> option,IConfiguration configuration, AzureServiceBusFactory serviceBus, HttpTrigger httpTrigger
+        private readonly IHttpClientFactory _httpClient;
+        public StudentController(IWebHostEnvironment environment, CoreAPIHttpService coreAPIHttpService, AzureCosmosFactory azureCosmos, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, DingDing dingDing, IPSearcher searcher, IOptionsSnapshot<Option> option,IConfiguration configuration, AzureServiceBusFactory serviceBus, HttpTrigger httpTrigger, IHttpClientFactory httpClient
             )
         {
             _searcher = searcher;
@@ -66,6 +69,7 @@ namespace TEAMModelOS.Controllers
             _httpTrigger = httpTrigger;
             _coreAPIHttpService = coreAPIHttpService;
             _environment = environment;
+            _httpClient = httpClient;
         }
 
         /// <summary>
@@ -703,6 +707,256 @@ namespace TEAMModelOS.Controllers
             var token = await CoreTokenExtensions.CreateAccessToken(clientID, clientSecret, _option.Location.Replace("-Dep", "").Replace("-Test", ""));
             return (auth_token, blob_uri, blob_sas, classinfo, courses, token);
         }
+        /// <summary>
+        /// 學生教育雲登入
+        /// </summary>
+        /// <param name = "request" ></ param >
+        [AllowAnonymous]
+        [HttpPost("login-open")]
+        public async Task<IActionResult> OpenIDLogin(JsonElement request)
+        {
+            try
+            {
+                var client = _azureCosmos.GetCosmosClient();
+                var schoolClient = client.GetContainer(Constant.TEAMModelOS, "School");
+                var teacherClient = client.GetContainer(Constant.TEAMModelOS, "Teacher");
+                var studentClient = client.GetContainer(Constant.TEAMModelOS, "Student");
+                //參數取得
+                string location = _option.Location;
+                if (!request.TryGetProperty("open_code", out JsonElement _open_code)) return BadRequest();
+                if (!location.Contains("Global")) return BadRequest();
+                string grant_type = "educloudtw";
+                string client_id = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
+                string redirect_uri = _configuration.GetValue<string>("HaBookAuth:CoreAccountAPI");
+                string nounce = RandomString(16);
+                string lang = "zh-tw";
+                string open_code = _open_code.GetString();
+                if(!open_code.Contains("EduCloudTWL")) return BadRequest();
+                bool is_extrnal_id = true;
+                //向CS取得OpenData
+                stuOpenData openData = new stuOpenData();
+                string csv2Domain = _configuration.GetValue<string>("HaBookAuth:CoreAPI");
+                string csv2Url = $"{csv2Domain}/oauth2/Login";
+                Dictionary<string, object> dict = new() { 
+                    {   "grant_type", grant_type },
+                    {   "client_id", client_id },
+                    {   "redirect_uri", $"{redirect_uri}/" },
+                    {   "nounce", nounce },
+                    {   "lang", lang },
+                    {   "open_code", open_code },
+                    {   "is_extrnal_id", is_extrnal_id }
+                };
+                var clientID = _configuration.GetValue<string>("HaBookAuth:CoreService:clientID");
+                var clientSecret = _configuration.GetValue<string>("HaBookAuth:CoreService:clientSecret");
+                var csToken = await CoreTokenExtensions.CreateAccessToken(clientID, clientSecret, location);
+
+                var httpClient = _httpClient.CreateClient();
+                if (httpClient.DefaultRequestHeaders.Contains("Authorization"))
+                {
+                    httpClient.DefaultRequestHeaders.Remove("Authorization");
+                }
+                httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {csToken.AccessToken}");
+                string test = dict.ToJsonString();
+                HttpContent content = new StringContent(dict.ToJsonString(), Encoding.UTF8, "application/json");
+                HttpResponseMessage httpResponse = await httpClient.PostAsync(csv2Url, content);
+                if (httpResponse.StatusCode == HttpStatusCode.OK)
+                {
+                    string responseContent = await httpResponse.Content.ReadAsStringAsync();
+                    openData = responseContent.ToObject<stuOpenData>();
+                    if (string.IsNullOrWhiteSpace(openData.open_id) || string.IsNullOrWhiteSpace(openData.schoolCode))
+                    {
+                        return Ok(new { error = 1, message = "Can not get opendata from CS." });
+                    }
+                }
+                else
+                {
+                    return Ok(new { error = 1, message = "Can not get opendata from CS." });
+                }
+                (string ip, string region) = await LoginService.LoginIp(HttpContext, _searcher);
+                //用OpenData取得學校資訊
+                School school = new School();
+                try
+                {
+                    school = await schoolClient.ReadItemAsync<School>($"{openData.schoolCode}", new PartitionKey("Base"));
+                }
+                catch (CosmosException ex) 
+                {
+                    return Ok(new { error = 2, message = "Can not find school data." });
+                }
+                //用OpenData取得學生資訊
+                Student stuinfo = new Student();
+                var queryLogin = $"SELECT * FROM c WHERE IS_DEFINED(c.openid) AND c.openid = '{openData.open_id}'";
+                await foreach (var item in studentClient.GetItemQueryStreamIterator(queryText: queryLogin, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Base-{openData.schoolCode}") }))
+                {
+                    using var json = await JsonDocument.ParseAsync(item.ContentStream);
+                    if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                    {
+                        foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                        {
+                            stuinfo = obj.ToObject<Student>();
+                        }
+                    }
+                }
+                //分歧1 有此學生 => Login流程
+                if(!string.IsNullOrWhiteSpace(stuinfo.id))
+                {
+                    if (stuinfo.graduate.Equals(1))
+                    {
+                        return Ok(new { error = 3, message = "Graduate already!" });
+                    }
+                    (string auth_token, string blob_uri, string blob_sas, object classinfo, List<object> courses, AuthenticationResult token) = await StudentCheck(school, $"{stuinfo.id}", $"{stuinfo.classId}", $"{openData.schoolCode}", $"{stuinfo.picture}", $"{stuinfo.name}", schoolClient, teacherClient, school.areaId, ip, client, stuinfo);
+                    //授权规模数量
+                    DateTimeOffset dateTime = DateTimeOffset.UtcNow;
+                    var dateDay = dateTime.ToString("yyyyMMdd"); //获取当天的日期
+                    string key = $"Login:School:{openData.schoolCode}:student-day:{dateDay}";
+                    SortedSetEntry[] countStudent = _azureRedis.GetRedisClient(8).SortedSetRangeByScoreWithScores(key);
+                    int countAuthorized = 0;
+                    if (countStudent != null && countStudent.Length > 0)
+                    {
+                        bool notify = false;
+                        countAuthorized = countStudent.Length;
+                        if (school.scale > 0 && school.scale - countAuthorized <= 0)
+                        {
+                            //登录人数已达授权规模数上限
+                            if (!string.IsNullOrWhiteSpace(school.areaId))
+                            {
+                                AreaSetting areaSetting = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Normal).ReadItemAsync<AreaSetting>(school.areaId, new PartitionKey("AreaSetting"));
+                                if (areaSetting.ignoreScaleExpire > dateTime.ToUnixTimeMilliseconds())
+                                {
+                                    //将人数控制在最大规模数以下。
+                                    countAuthorized = school.scale - 1;
+                                }
+                                else
+                                {
+                                    notify = true;
+                                }
+                            }
+                            else
+                            {
+                                notify = true;
+                            }
+                            if (notify)
+                            {
+                                //通知key 一天只通知一次
+                                string scaleNotifykey = $"Login:School:{school.id}:student-scale-notify:{dateDay}";
+                                bool Exists = await _azureRedis.GetRedisClient(8).KeyExistsAsync(scaleNotifykey);
+                                if (!Exists)
+                                {
+                                    //获取学校管理员
+                                    List<IdNameCode> ids = new List<IdNameCode>();
+                                    string sql = $"select   value c from c    where c.code='Teacher-{school.id}' and c.status='join' and array_contains(c.roles,'admin') ";
+                                    List<SchoolTeacher> adminTeachers = new List<SchoolTeacher>();
+                                    await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School)
+                                        .GetItemQueryIterator<SchoolTeacher>(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Teacher-{school.id}") }))
+                                    {
+                                        adminTeachers.Add(item);
+                                    }
+                                    if (adminTeachers.IsNotEmpty())
+                                    {
+
+                                        string sqlAdmin = $"select c.id,c.lang  as code ,c.name from c where c.id in ({string.Join(",", adminTeachers.Select(z => $"'{z.id}'"))}) ";
+                                        await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher)
+                                            .GetItemQueryIterator<IdNameCode>(queryText: sqlAdmin, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Base") }))
+                                        {
+                                            ids.Add(item);
+                                        }
+                                        foreach (var uds in ids)
+                                        {
+                                            _coreAPIHttpService.PushNotify(new List<IdNameCode> { uds }, $"school-scale-notify", Constant.NotifyType_IES5_Management, new Dictionary<string, object> { { "tmdname", uds.name }, { "countAuthorized", $"{countAuthorized}" }, { "scale", $"{school.scale}" }, { "schoolName", school.name }, { "schoolId", $"{school.id}" }, }, _option.Location, _configuration, _dingDing, _environment.ContentRootPath);
+                                        }
+                                        await _azureRedis.GetRedisClient(8).StringSetAsync(scaleNotifykey, scaleNotifykey, new TimeSpan(hours: 24, minutes: 0, seconds: 0));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    return Ok(new { school.scale, countAuthorized, location = _option.Location, error = 0, auth_token, blob_uri, blob_sas, classinfo, courses, token = new { access_token = token.AccessToken, expires_in = token.ExpiresOn, id_token = auth_token, token_type = token.TokenType } });
+                }
+                //分歧2 無此學生 => 取得該校同名學生資訊
+                else
+                {
+                    //學段
+                    Dictionary<string, string> periodDic = new Dictionary<string, string>();
+                    foreach(Period stuSchoolPeriod in school.period)
+                    {
+                        periodDic.Add(stuSchoolPeriod.id, stuSchoolPeriod.name);
+                    }
+                    //取得同名學生
+                    HashSet<string> classIds = new HashSet<string>();
+                    List<Student> stuList = new List<Student>();
+                    var queryStuSame = $"SELECT * FROM c WHERE c.name = '{openData.open_name}'";
+                    await foreach (var item in studentClient.GetItemQueryStreamIterator(queryText: queryStuSame, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Base-{openData.schoolCode}") }))
+                    {
+                        using var json = await JsonDocument.ParseAsync(item.ContentStream);
+                        if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                        {
+                            foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                            {
+                                Student stuRow = obj.ToObject<Student>();
+                                stuList.Add(stuRow);
+                                if(string.IsNullOrWhiteSpace(stuRow.classId))
+                                {
+                                    classIds.Add(stuRow.classId);
+                                }
+                            }
+                        }
+                    }
+                    //取得學校班級
+                    Dictionary<string, string> classDic = new Dictionary<string, string>();
+                    string classIdJsonStr = JsonSerializer.Serialize(classIds);
+                    var query = $"SELECT c.code, c.id, c.name, c.periodId, c.gradeId FROM c WHERE ARRAY_CONTAINS({classIdJsonStr}, c.id)";
+                    await foreach (var item in schoolClient.GetItemQueryStreamIterator(queryText: query, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{openData.schoolCode}") }))
+                    {
+                        using var json = await JsonDocument.ParseAsync(item.ContentStream);
+                        if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                        {
+                            foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                            {
+                                Class classinfo = obj.ToObject<Class>();
+                                classDic.Add(classinfo.id, classinfo.name);
+                            }
+                        }
+                    }
+                    //回傳值
+                    List<stuOpenDataOrientation> result = new List<stuOpenDataOrientation>();
+                    foreach (Student studata in stuList)
+                    {
+                        stuOpenDataOrientation stuResultRow = new stuOpenDataOrientation();
+                        stuResultRow.id = studata.id;
+                        stuResultRow.name = studata.name;
+                        stuResultRow.schoolId = studata.schoolId;
+                        stuResultRow.schoolName = school.name;
+                        stuResultRow.classId = studata.classId;
+                        stuResultRow.className = (!string.IsNullOrWhiteSpace(studata.classId) && classDic.ContainsKey(studata.classId)) ? classDic[studata.classId] : string.Empty;
+                        stuResultRow.periodId = studata.periodId;
+                        stuResultRow.periodName = (!string.IsNullOrWhiteSpace(studata.periodId) && periodDic.ContainsKey(studata.periodId)) ? periodDic[studata.periodId] : string.Empty;
+                        stuResultRow.year = studata.year;
+                        stuResultRow.gender = studata.gender;
+                        Period curPeriod = new Period();
+                        if (!string.IsNullOrWhiteSpace(studata.periodId))
+                        {
+                            curPeriod = school.period.Where(p => p.id.Equals(studata.periodId)).FirstOrDefault();
+                        }
+                        ExamSimple gradeInfo = new ExamSimple();
+                        if (string.IsNullOrWhiteSpace(curPeriod.id))
+                        {
+                            gradeInfo = getGradeInfoByYear(studata.year, curPeriod);
+                        }
+                        stuResultRow.gradeIndex = (!string.IsNullOrWhiteSpace(gradeInfo.id)) ? gradeInfo.id : string.Empty;
+                        stuResultRow.gradeName = (!string.IsNullOrWhiteSpace(gradeInfo.name)) ? gradeInfo.name : string.Empty;
+                        result.Add(stuResultRow);
+                    }
+                    //回傳值
+                    return Ok(result);
+                }
+            }
+            catch (Exception ex)
+            {
+                await _dingDing.SendBotMsg($"OS,{_option.Location},student/openlogin()\n{ex.Message}\n{ex.StackTrace}\n", GroupNames.醍摩豆服務運維群組);
+                return BadRequest();
+            }
+        }
+
         //查询学生名单详情
         [ProducesDefaultResponseType]
         //[AuthToken(Roles = "teacher")]
@@ -945,5 +1199,65 @@ namespace TEAMModelOS.Controllers
                 return BadRequest();
             }
         }
+        /**
+         * 根据学年获取年级信息
+         * @param year 学年
+         * @param Period 学段資料
+         */
+        private ExamSimple getGradeInfoByYear(int year, Period curPeriod)
+        {
+            ExamSimple result = new ExamSimple();
+            if (year > 0)
+            {
+                DateTime date = DateTime.UtcNow;
+                int curYear = date.Year;
+                int month = date.Month;
+                Semester semesterStart = curPeriod.semesters.Where((Semester x) => x.start.Equals(1)).FirstOrDefault();
+                if (semesterStart != null)
+                {
+                    if (month < semesterStart.month)
+                    {
+                        curYear--;
+                    }
+                    int gradeIndex = curYear - year;
+                    result.id = gradeIndex.ToString();
+                    result.name = (gradeIndex >= curPeriod.grades.Count) ? "graduated" : (gradeIndex >= 0) ? curPeriod.grades[gradeIndex] : "not-enrollment";
+                }
+            }
+            return result;
+        }
+
+        public static string RandomString(int length)
+        {
+            Random random = new Random();
+            const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+            return new string(Enumerable.Repeat(chars, length)
+                .Select(s => s[random.Next(s.Length)]).ToArray());
+        }
+
+        //取得學生OpenData
+        private class stuOpenData
+        {
+            public string open_id { get; set; }
+            public string open_name { get; set; }
+            public string open_mail { get; set; }
+            public string schoolCode { get; set; }
+        }
+        //無法取得OpenID搜尋學生姓名回傳的學生資料
+        private class stuOpenDataOrientation
+        {
+            public string id { get; set; }
+            public string name { get; set; }
+            public string schoolId { get; set; }
+            public string schoolName { get; set; }
+            public string classId { get; set; }
+            public string className { get; set; }
+            public string periodId { get; set; }
+            public string periodName { get; set; }
+            public int year { get; set; }
+            public string gender { get; set; }
+            public string gradeIndex { get; set; }
+            public string gradeName { get; set; }
+        }
     }
 }