|
@@ -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; }
|
|
|
+ }
|
|
|
}
|
|
|
}
|